首页/开发/代码质量与评审速查/手法:Change Reference to Value / Change Value to Reference(值引用互换)
重构手法62 条

手法:Change Reference to Value / Change Value to Reference(值引用互换)

按相等性语义选择对象模型:值对象按内容相等(钱、坐标),引用对象共享实例(客户、账户);模型选错的典型症状是相等判断与更新语义别扭。

是什么

值对象与引用对象互换针对的是相等性语义的建模选择。值对象按内容相等,两个内容相同的钱或坐标就应当被视为同一个值,通常设计为不可变;引用对象按标识相等,同一个客户或账户即使属性发生变化,仍然是同一个实体,通过共享实例来保证更新全局可见。模型选错的典型症状是相等判断与更新语义处处别扭:要么需要不断比较所有字段,要么到处拷贝实例导致同一实体出现多份。

选择依据

值对象不可变、按内容相等、可自由拷贝与缓存
引用对象共享实例、按标识相等、更新对所有持有者可见
判断问题该对象是否代表一个可辨识的实体,而非一组数据
典型误用把钱做成可变对象,或把客户做成随意拷贝的结构

典型使用场景

  • 金额与币种:应做成不可变值对象,相等即内容相同,避免共享修改
  • 坐标与时间区间:按内容相等更符合业务直觉,便于比较与缓存
  • 客户与账户:属于可辨识实体,应共享同一实例,避免多份副本状态不一致
  • 配置项集合:若整体替换而非原地修改,用值对象更安全
  • 缓存键:用值语义保证内容相同即命中同一缓存

示意

示意
# 值对象:按内容相等,可自由拷贝与比较
Money.of("CNY", 10.00).equals(Money.of("CNY", 10.00))   → true

# 引用对象:按标识相等,共享同一实例
customerA == customerRepository.find(customerA.id)        → true

# 判断问句:这个对象是否代表一个可辨识的实体?

互换方向与检查

当前形态问题信号互换方向
值对象但实现了可变缓存与前缀共享出现脏数据改为不可变,更新时整体替换
值对象但用标识相等内容相同的两份数据无法比较改为按内容实现相等与哈希
引用对象但到处拷贝同一实体出现多份副本,更新丢失改为共享实例,通过标识查找
引用对象但用于缓存键属性变化导致缓存失效混乱抽出值语义的键类型
实体与值混在一个类里相等语义自相矛盾拆成实体与值两个类型
值对象一旦设计为可变,所有依赖内容相等的机制都会变得脆弱:缓存键失效、集合去重失真、比较结果随修改漂移。让值对象不可变是最省事的做法。
引用对象需要统一入口来获取实例,否则同一个实体容易在不同模块各被构造一份。仓库或注册表形式的入口能保证标识相等,也让更新对所有持有者可见。

落地检查清单

  • 相等判断方式与业务语义一致
  • 值对象不可变,更新通过替换完成
  • 引用对象通过统一入口获取,避免多份副本
  • 相等的对象哈希值一致,可在集合与缓存中正常使用

易错点

  • 值对象实现为可变:缓存与共享前缀出现脏数据,问题表现为随机且难以复现
  • 引用对象到处拷贝实例:同一实体出现多个副本,更新只作用于其中一份,数据长期不一致
  • 只重写相等判断不重写哈希:对象放入集合或用作缓存键时出现重复或查找不到
  • 实体与值语义混用:同一个类既按标识比较又按内容比较,调用方无法预期行为

数据来源与参考

  • 《重构》第 2 版 · 第 9 章、第 11 章(Change Value to Reference / Change Reference to Value)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌