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)不应该同时回答问题。


相关推荐
怕浪猫12 小时前
一行命令复刻爆款视频,我把 Hypit 从安装跑到了出片
人工智能·设计模式·程序员
Zane199414 小时前
函数式编程里的函数,其实不是你天天写的那个函数——三大编程范式的边界在哪
设计模式
Zane19942 天前
策略模式现在该不该上?一次讲清楚过度设计和设计不足怎么找平衡
设计模式
她说..2 天前
常见设计模式-模板方法模式
java·spring·设计模式·springboot
xiaofeiyang1502 天前
第六章 · 桥接 — 三支毛笔,画出九种颜色
设计模式
Shadow(⊙o⊙)3 天前
OTOL设计模式 One Thread One Loop
服务器·网络·设计模式
sarasuki3 天前
如何让LLM 能在半夜偷偷打开网易云呢?
人工智能·设计模式·agent
小王师傅663 天前
【设计模式】装饰模式(四):框架源码实战——从 Java I/O 到 Spring 到 MyBatis
java·设计模式
执明wa4 天前
Android RecyclerView 多类型, 多种 Item
android·xml·开发语言·设计模式·android studio
sarasuki4 天前
如何让 Agent 获取更多的能力?插件 / 技能系统
人工智能·设计模式·agent