是什么
命令与查询分离指的是:一个函数要么回答一个问题且不改变状态,要么执行一个动作且不返回业务结果,避免把两者混在一处。同时承担返回与修改的函数最常见的坑是重复执行——调用方为了看结果而调用一次,为了执行又调用一次,业务动作被做了两遍。把查询与命令拆开后,查询可以安全地重复调用、自由缓存,命令的副作用范围也更容易看清。
识别与做法
| 坏味道 | 命令查询混用,调用方无法从中看出是否有副作用 |
|---|---|
| 做法 | 拆分为查询函数与命令函数,名称分别体现读与写 |
| 收益 | 查询可安全重复调用与缓存,命令的副作用边界清晰 |
| 注意 | 拆分后要更新全部调用点,避免动作被执行两次 |
典型使用场景
- 栈的取值与弹出:把窥视与弹出分成两个方法,避免只想看一眼却改变了数据结构
- 集合的查找与初始化:查找方法不应顺手创建元素,否则调用方误以为只是查询
- 计数与递增:读取计数与加一分开,便于排查重复计数问题
- 状态判断与状态迁移:先查询是否可执行,再执行命令,职责清晰
- 缓存读取:查询方法不应顺带刷新缓存,刷新应由显式命令触发
示例
示例
# 调整前:一个方法既返回又删除,调用两次就删两次
value = queue.take() # 取值并移除,重复调用有副作用
# 调整后:查询与命令分开,各自职责单一
value = queue.peek() # 只查看,可安全重复调用
value = queue.take() # 明确的动作,仅执行一次拆分时检查
| 检查项 | 关注点 |
|---|---|
| 调用点替换 | 原来只读的调用是否被误改成命令,导致数据被多改一次 |
| 返回语义 | 命令函数是否仍需要返回执行结果,若需要则改为返回结果对象 |
| 异常路径 | 查询在数据缺失时返回空值还是抛异常,需与调用方约定一致 |
| 并发与重试 | 查询可重复执行后,是否被用于重试逻辑,需确认幂等 |
| 惰性初始化 | 查询中隐藏的初始化写入是否被移到显式命令中 |
查询函数里最隐蔽的副作用是惰性初始化:表面只是读取,实际上会写入缓存或创建对象。这类方法在并发与测试环境下容易表现出不一致,最好把初始化显式化。
拆分后命令函数仍可能需要返回标识或结果:此时改为返回一个结果对象,而不是把状态混在返回值里,让调用方明确知道这是一次动作的产出。
落地检查清单
- 查询函数没有写入副作用,可重复调用
- 命令函数有清晰命名,调用点能看出是动作
- 全部调用点已更新,没有把动作执行两次
- 缺失数据的返回约定在调用方之间保持一致