首页/开发/代码质量与评审速查/手法:Replace Return Value with ... / 拆出修改与查询
重构手法62 条

手法:Replace Return Value with ... / 拆出修改与查询

函数同时承担「回答一个问题」与「改变状态」时,按查询与命令分离:要么返回结果并保持无副作用,要么只做动作不回值;副作用与返回值混杂最常见的坑是重复执行。

是什么

命令与查询分离指的是:一个函数要么回答一个问题且不改变状态,要么执行一个动作且不返回业务结果,避免把两者混在一处。同时承担返回与修改的函数最常见的坑是重复执行——调用方为了看结果而调用一次,为了执行又调用一次,业务动作被做了两遍。把查询与命令拆开后,查询可以安全地重复调用、自由缓存,命令的副作用范围也更容易看清。

识别与做法

坏味道命令查询混用,调用方无法从中看出是否有副作用
做法拆分为查询函数与命令函数,名称分别体现读与写
收益查询可安全重复调用与缓存,命令的副作用边界清晰
注意拆分后要更新全部调用点,避免动作被执行两次

典型使用场景

  • 栈的取值与弹出:把窥视与弹出分成两个方法,避免只想看一眼却改变了数据结构
  • 集合的查找与初始化:查找方法不应顺手创建元素,否则调用方误以为只是查询
  • 计数与递增:读取计数与加一分开,便于排查重复计数问题
  • 状态判断与状态迁移:先查询是否可执行,再执行命令,职责清晰
  • 缓存读取:查询方法不应顺带刷新缓存,刷新应由显式命令触发

示例

示例
# 调整前:一个方法既返回又删除,调用两次就删两次
value = queue.take()        # 取值并移除,重复调用有副作用

# 调整后:查询与命令分开,各自职责单一
value = queue.peek()        # 只查看,可安全重复调用
value = queue.take()        # 明确的动作,仅执行一次

拆分时检查

检查项关注点
调用点替换原来只读的调用是否被误改成命令,导致数据被多改一次
返回语义命令函数是否仍需要返回执行结果,若需要则改为返回结果对象
异常路径查询在数据缺失时返回空值还是抛异常,需与调用方约定一致
并发与重试查询可重复执行后,是否被用于重试逻辑,需确认幂等
惰性初始化查询中隐藏的初始化写入是否被移到显式命令中
查询函数里最隐蔽的副作用是惰性初始化:表面只是读取,实际上会写入缓存或创建对象。这类方法在并发与测试环境下容易表现出不一致,最好把初始化显式化。
拆分后命令函数仍可能需要返回标识或结果:此时改为返回一个结果对象,而不是把状态混在返回值里,让调用方明确知道这是一次动作的产出。

落地检查清单

  • 查询函数没有写入副作用,可重复调用
  • 命令函数有清晰命名,调用点能看出是动作
  • 全部调用点已更新,没有把动作执行两次
  • 缺失数据的返回约定在调用方之间保持一致

易错点

  • 拆分后旧调用点忘记更新:同一动作被执行两次,数据出现重复变更,问题难以在测试中暴露
  • 查询函数里做惰性初始化:表面无副作用实则写入,并发与重试场景下出现不一致结果
  • 命令函数继续返回业务结果:调用方仍然依赖返回值判断状态,命令查询再次混用
  • 把校验逻辑放进命令:查询路径与命令路径行为不一致,调用方在同一份数据上得到不同结论

数据来源与参考

  • 《重构》第 2 版 · 第 11 章(Separate Query from Modifier 等)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌