CQS - 命令查询分离:驯服副作用

📋 Research Summary

命令查询分离(Command Query Separation, CQS)是由 Bertrand Meyer 提出的设计原则,它将所有方法分为两类:**命令(Command)**负责改变状态但不返回值,**查询(Query)**负责返回值但不改变状态。这一看似简单的原则,是消除"海森堡Bug(观察者效应)"和降低代码认知负荷的良药,也是现代架构模式 CQRS 的理论基石。


🌱 逻辑原点

你打开冰箱门只是为了看看有没有牛奶(Query),结果冰箱因为这个动作自动订购了五箱牛奶(Side Effect)。

在软件开发中,我们最害怕的不是"复杂的逻辑",而是"隐形的副作用"。
当我们调用一个返回值的方法时,默认假设它是安全的、幂等的;而当我们调用一个无返回值的方法时,我们才预期状态会改变。

如果一个名为 checkAuth() 的方法在返回 true 的同时,悄悄修改了用户的 lastLoginTime 甚至刷新了 Token,这种"挂羊头卖狗肉"的行为就是无数调试噩梦的根源。

🧠 苏格拉底式对话

1️⃣ 现状:最原始的解法是什么?

混合方法(Mixed Methods)。

最典型的例子是栈的 pop() 方法:

  • 它移除栈顶元素(改变状态,Command)。
  • 它同时返回这个元素(返回数据,Query)。
    优点:方便,一行代码搞定读取和删除。

2️⃣ 瓶颈:规模扩大 100 倍时会在哪里崩溃?

状态预测的不确定性。

假设你有一个复杂的计费系统,你只想在日志里记录一下当前用户的余额。

你调用了 account.getBalanceAndDeductFee()

结果,每次打印日志,用户的钱就被扣一次。

当系统中有成百上千个这样的"混合方法",每一次读取数据都像是在拆弹,你永远不知道哪个 Getter 里面藏着一个 Setter。代码的可测试性和可维护性急剧下降。

3️⃣ 突破:必须引入什么新维度?

严格的职责二分(Strict Separation)。

我们将所有操作强制分为两类:

  • Queries : Return T, Void Side Effects. (只读,安全,可随时调用,可缓存)
  • Commands : Return Void, Has Side Effects. (只写,危险,需谨慎调用)
    对于 pop(),我们将其拆分为:
  • top(): 只返回栈顶元素,不删除。
  • remove(): 只删除栈顶元素,不返回。
    调用者必须显式地写两行代码,但这消除了歧义。

📊 视觉骨架

✅ CQS 遵循 (分离) ⚠️ CQS 违规 (混合) 返回值 + 修改状态
getAndIncrement
查询 (Query) 返回值 (无副作用)
getValue
命令 (Command) 修改状态 (无返回值)
increment
幂等,可重试

可并行,可缓存
非幂等

需事务控制

严格顺序

⚖️ 权衡模型

公式:

复制代码
CQS = (可预测性 + 读写优化空间) - (代码行数 × 2)

代价分析:

  • 解决: 海森堡 Bug。消除了"观察即改变"的副作用,读取数据永远安全。
  • 解决: 读写性能不对称。为架构升级到 CQRS(读写分离架构)打下基础,读模型和写模型可以独立优化。
  • 牺牲: 原子性便利pop() 操作变成了 top() + remove(),在多线程环境下,这两步操作之间可能插入其他线程的操作,需要额外的同步机制(Locking)。
  • ⚠️ 误区: CQS 不是 CQRS。CQS 是方法/类级别的原则,CQRS 是系统/架构级别的模式。做 CQS 不一定非要搞两个数据库。

🔁 记忆锚点

typescript 复制代码
// ❌ 违规:Getter 居然修改了状态!
function getScore(studentId: string): number {
    const score = db.find(studentId);
    // 😱 居然在这里扣分!
    db.update(studentId, score - 1); 
    return score;
}

// ✅ 遵循:泾渭分明
function getScore(studentId: string): number {
    return db.find(studentId); // 纯净,无副作用
}

function deductScore(studentId: string): void {
    const score = db.find(studentId);
    db.update(studentId, score - 1); // 明确的命令
}

一句话本质: 问问题(Query)不应该改变答案(State),改变答案(Command)不应该同时回答问题。


相关推荐
geovindu10 小时前
python: Template Method Pattern
开发语言·python·设计模式·模板方法模式
HY小海16 小时前
【Unity游戏创作】常见的设计模式
unity·设计模式·c#·游戏程序
Yongqiang Cheng16 小时前
设计模式:C++ 模板方法模式 (Template Method in C++)
设计模式·template method·c++ 模板方法模式
我爱cope16 小时前
【从0开始学设计模式-3| 工厂模式】
设计模式
资深web全栈开发1 天前
设计模式之空对象模式 (Null Object Pattern)
设计模式
我爱cope1 天前
【从0开始学设计模式-2| 面向对象设计原则】
设计模式
资深web全栈开发2 天前
设计模式之访问者模式 (Visitor Pattern)
设计模式·访问者模式
sg_knight2 天前
对象池模式(Object Pool)
python·设计模式·object pool·对象池模式
Yongqiang Cheng2 天前
设计模式:C++ 单例模式 (Singleton in C++)
设计模式·c++ 单例模式