第42课:CQS 方法论(命令查询分离)

概述

CQS(Command-Query Separation,命令查询分离)是由 Bertrand Meyer 提出的设计原则:每个方法要么是命令(修改状态),要么是查询(返回数据),但不能兼而有之。


核心定义

graph TB
    CQS[”CQS 命令查询分离”] --> Command[”Command 命令”]
    CQS --> Query[”Query 查询”]

    Command --> C1[”修改状态”]
    Command --> C2[”有副作用”]
    Command --> C3[”通常返回 void”]

    Query --> Q1[”返回数据”]
    Query --> Q2[”无副作用”]
    Query --> Q3[”可重复调用”]
类型英文行为副作用返回值
命令Command / Modifier修改系统状态有副作用通常 void
查询Query读取数据无副作用返回数据

违反 CQS 的反例

问题:一个方法又查又删

// ❌ 违反 CQS:closeUserBind 同时做了两件事
public class UserService {
 
    public void closeUserBind(String username) {
        // 查询:读取用户信息
        var user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UserNotFoundException(username);
        }
 
        // ... 一些业务操作 ...
 
        // 命令:删除用户
        userRepository.deleteById(user.getId());
    }
}

时序问题

// 更严重的时序问题:先查再删
public void closeUserBind(String username) {
    var user = userRepository.findByUsername(username);
    // ❌ 此时可能有其他线程删除了 user
    // 但下面的操作还假设 user 仍然存在
    auditService.log("Closing user: " + user.getId());
    userRepository.deleteById(user.getId());
}

CQS 解决方案

重构:查询与命令分离

graph LR
    subgraph ”CQS 分离前(违反原则)”
        BEFORE[”closeUserBind(username)<br/>🔍 查用户 + 🗑️ 删用户”]
    end

    subgraph ”CQS 分离后(符合原则)”
        QUERY[”findByUsername(username)<br/>🔍 纯查询 → 返回 User”]
        COMMAND[”deleteUser(userId)<br/>🗑️ 纯命令 → 删除”]
    end

    BEFORE --> QUERY
    BEFORE --> COMMAND
// ✅ 遵循 CQS:查询与命令分离
public class UserQueryService {
    // 查询:纯读取,无副作用
    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }
}
 
public class UserCommandService {
    // 命令:执行删除,有副作用
    public void deleteUser(Long userId) {
        if (!userRepository.existsById(userId)) {
            throw new UserNotFoundException(userId);
        }
        userRepository.deleteById(userId);
    }
}

在 Controller 中组合

@RestController
@RequestMapping("/api/users")
public class UserController {
 
    private final UserQueryService userQueryService;
    private final UserCommandService userCommandService;
 
    public UserController(UserQueryService userQueryService,
                          UserCommandService userCommandService) {
        this.userQueryService = userQueryService;
        this.userCommandService = userCommandService;
    }
 
    @PostMapping("/closeBind")
    public void closeBind(@RequestParam String username) {
        // Controller 编排:先查后删
        var user = userQueryService.findByUsername(username);
        userCommandService.deleteUser(user.getId());
    }
}

CQS 驱动架构平衡:避免”干巴巴的 Controller”

问题:干巴巴的 Controller

// ❌ "干巴巴的 Controller":Controller 只有一行转发
@PostMapping("/closeBind")
public void closeBind(@RequestParam String username) {
    userService.closeUserBind(username); // 所有逻辑下沉到 Service
}

这种 Controller 称为”干巴巴的 Controller”(Anemic Controller),它不包含任何业务编排逻辑,只是一个薄薄的代理。这通常是违反 CQS 的信号——Service 层同时做了查询和命令的事情。

CQS 改进后

// ✅ Controller 有了自己的行为逻辑
@PostMapping("/closeBind")
public void closeBind(@RequestParam String username) {
    var user = userQueryService.findByUsername(username);  // Controller 做查询
    userCommandService.deleteUser(user.getId());            // Controller 做命令
}

CQS 分离前后的架构对比

graph TB
    subgraph ”CQS 分离前”
        C1[”Controller”] --> S1[”UserService<br/>(查询+命令混合)”]
        S1 --> R1[”UserRepository”]
    end

    subgraph ”CQS 分离后”
        C2[”Controller<br/>(编排查询+命令)”] --> QS[”UserQueryService<br/>(纯查询)”]
        C2 --> CS[”UserCommandService<br/>(纯命令)”]
        QS --> R2[”UserRepository”]
        CS --> R2
    end

CQS 与单元测试

CQS 让测试边界更清晰:

// 查询测试:只验证返回值
@Test
void findByUsername_whenUserExists_willReturnUser() {
    var user = userQueryService.findByUsername("test");
 
    assertNotNull(user);
    assertEquals("test", user.getUsername());
}
 
// 命令测试:只验证副作用
@Test
void deleteUser_whenUserExists_willRemoveFromDB() {
    userCommandService.deleteUser(1L);
 
    verify(userRepository).deleteById(1L);
}
 
@Test
void deleteUser_whenUserNotExists_willThrowException() {
    when(userRepository.existsById(99L)).thenReturn(false);
 
    assertThrows(UserNotFoundException.class,
            () -> userCommandService.deleteUser(99L));
}

CQS 在微服务层面的扩展

CQS 原则可以从方法级别扩展到服务级别

级别命令查询
方法级deleteUser(id)findByUsername(name)
类级UserCommandServiceUserQueryService
服务级订单写入服务订单查询服务

在微服务架构中,CQS 可演化为 CQRS(Command Query Responsibility Segregation),即完全的读写分离架构。


CQS 的边界:何时可以适度打破

CQS 是原则而非绝对铁律。以下情况可以适度打破:

// 合理的例外:返回 ID 的创建方法
// 虽然修改了状态(命令),但返回了新创建记录的 ID(查询)
public Long createUser(String username, String email) {
    var user = new User(username, email);
    userRepository.save(user);
    return user.getId(); // 返回生成的 ID
}

原则: 可以打破,但要意识到你在打破,并且有充分的理由。


语言迁移

CQS 是普适的设计原则。 Python 开发者可以将 Service 拆分为 UserQueryService / UserCommandService;JavaScript 开发者可以在 NestJS 中实现独立的 Query/Command Handler。CQS 的核心价值——分离读写职责、明确方法意图——在任何语言中都能提升代码质量。


静态收束

CQS 将方法清晰地划分为”命令”和”查询”两类,消除了”一个方法既查又改”的模糊地带。这不仅让代码的意图更加明确,也让测试边界更加清晰——查询只测返回值,命令只测副作用。更重要的是,CQS 驱动了更健康的架构:Controller 不再是”干巴巴的”代理,而是真正的行为编排者。