是什么
值对象与引用对象互换针对的是相等性语义的建模选择。值对象按内容相等,两个内容相同的钱或坐标就应当被视为同一个值,通常设计为不可变;引用对象按标识相等,同一个客户或账户即使属性发生变化,仍然是同一个实体,通过共享实例来保证更新全局可见。模型选错的典型症状是相等判断与更新语义处处别扭:要么需要不断比较所有字段,要么到处拷贝实例导致同一实体出现多份。
选择依据
| 值对象 | 不可变、按内容相等、可自由拷贝与缓存 |
|---|---|
| 引用对象 | 共享实例、按标识相等、更新对所有持有者可见 |
| 判断问题 | 该对象是否代表一个可辨识的实体,而非一组数据 |
| 典型误用 | 把钱做成可变对象,或把客户做成随意拷贝的结构 |
典型使用场景
- 金额与币种:应做成不可变值对象,相等即内容相同,避免共享修改
- 坐标与时间区间:按内容相等更符合业务直觉,便于比较与缓存
- 客户与账户:属于可辨识实体,应共享同一实例,避免多份副本状态不一致
- 配置项集合:若整体替换而非原地修改,用值对象更安全
- 缓存键:用值语义保证内容相同即命中同一缓存
示意
示意
# 值对象:按内容相等,可自由拷贝与比较
Money.of("CNY", 10.00).equals(Money.of("CNY", 10.00)) → true
# 引用对象:按标识相等,共享同一实例
customerA == customerRepository.find(customerA.id) → true
# 判断问句:这个对象是否代表一个可辨识的实体?互换方向与检查
| 当前形态 | 问题信号 | 互换方向 |
|---|---|---|
| 值对象但实现了可变 | 缓存与前缀共享出现脏数据 | 改为不可变,更新时整体替换 |
| 值对象但用标识相等 | 内容相同的两份数据无法比较 | 改为按内容实现相等与哈希 |
| 引用对象但到处拷贝 | 同一实体出现多份副本,更新丢失 | 改为共享实例,通过标识查找 |
| 引用对象但用于缓存键 | 属性变化导致缓存失效混乱 | 抽出值语义的键类型 |
| 实体与值混在一个类里 | 相等语义自相矛盾 | 拆成实体与值两个类型 |
值对象一旦设计为可变,所有依赖内容相等的机制都会变得脆弱:缓存键失效、集合去重失真、比较结果随修改漂移。让值对象不可变是最省事的做法。
引用对象需要统一入口来获取实例,否则同一个实体容易在不同模块各被构造一份。仓库或注册表形式的入口能保证标识相等,也让更新对所有持有者可见。
落地检查清单
- 相等判断方式与业务语义一致
- 值对象不可变,更新通过替换完成
- 引用对象通过统一入口获取,避免多份副本
- 相等的对象哈希值一致,可在集合与缓存中正常使用