Skip to content

值对象最佳实践

本文档介绍 Pragmatic DDD 中值对象的设计与编码最佳实践:什么是值对象、与枚举/参数对象/实体的取舍、编写规范与常见反模式。前置阅读:聚合设计原则

1. 什么是值对象

值对象是由属性组合判等的可嵌入数据结构(如地址、金额、区间),不可变。核心特征是结构相等:两个值对象只要属性相同即相等,没有独立于属性的身份。

普通实体AbstractEntity)对比:实体的等同性由 entityId 决定,字段变化不改变身份;值对象没有身份,判等即比属性。

值对象是纯粹的数据载体:只负责承载字段值,不在内部校验字段——字段值的合法性统一由校验规则类执行(见 §5)。

2. 值对象 vs 枚举

类型特征例子
值对象(继承 ValueObject / 实现 IValueObject由多个字段组合、按结构判等的数据地址、金额、区间
枚举(实现 IEnumValue<T, 自身>固定离散常量集合订单状态、支付方式

经验法则:能用有限个常量表达的分类用枚举;需要由多个字段组合且按结构判等的数据用值对象。枚举的定义与持久化规范见 枚举值对象最佳实践

3. 值对象 vs 参数对象

类型定位特征
值对象领域内的可嵌入数据结构判等是领域行为;可持久化(MyBatis JSON 列)
参数对象(IParamObject入参载体纯数据容器、@Data 即可;不参与持久化、无值语义

4. 编写规范

4.1 继承 ValueObject 基类

结构相等用 ValueObject 基类,覆写 equalityComponents() 返回参与判等的成分(顺序敏感)。equals / hashCode / toString 由基类 final 提供,子类不可覆盖;相等判定要求精确类匹配(子类间即使成分相同也不相等)。

java
@Override
protected Object[] equalityComponents() {
    return new Object[]{province, city, detail};
}

4.2 实现 IValueObject 标记

复杂值对象通过 MyBatis JSON TypeHandler 整体读写数据库 JSON 列——必须实现 IValueObject 标记接口才会被自动登记。ValueObject 基类已实现 IValueObject,继承基类即自动满足标记。

4.3 不可变

  • 字段不提供公开 setter,判等由 ValueObject 基类托管
  • 通过全参业务构造完成初始化;构造期不做字段校验,字段值的合法性由校验规则类统一校验(见 §5)
  • 不可变性由使用者保证:Lombok 约定 @Getter + @Setter(AccessLevel.PROTECTED)(仅供持久化重建)+ @NoArgsConstructor(access = AccessLevel.PROTECTED)禁用 @Data / @EqualsAndHashCode / @Builder

4.4 示例

java
@Getter
@Setter(AccessLevel.PROTECTED)          // 仅供 MyBatis JSON 重建
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Address extends ValueObject {

    private String province;
    private String city;
    private String detail;

    public Address(String province, String city, String detail) {
        this.province = province;
        this.city = city;
        this.detail = detail;
    }

    @Override
    protected Object[] equalityComponents() {
        return new Object[]{province, city, detail};
    }
}

new Address("浙江", "杭州", "xxx").equals(new Address("浙江", "杭州", "xxx"))true

5. 校验职责:值对象不做校验

值对象是纯粹的数据载体,只负责承载字段值,不在内部校验字段(构造期、方法内都不做)。

字段值的合法性校验统一由校验规则类在应用层触发执行:

  • 内部不变量:在 EntityRule 子类的 init()addRule 编写
  • 外部依赖校验:定义校验规则领域服务(extends IDomainService),注入 EntityRule 执行

详见 聚合业务规则(OrderRule 范式)

为什么:同一值对象在不同业务上下文中合法性约束可能不同(如订单金额要求 > 0,退款金额要求 >= 0)。把校验写死在值对象内部会耦合固定、无法按场景复用;统一由规则类按场景校验,值对象保持纯粹、可预测。

6. 常见反模式

反模式问题正确做法
值对象提供公开 setter破坏不可变性@Setter(PROTECTED) 仅限持久化重建路径
值对象覆盖 equals/hashCodeValueObject 基类 final 冲突交给基类,只覆写 equalityComponents()
用值对象表达固定离散常量大量实例化、判断繁琐改用枚举 IEnumValue
用枚举承载多字段复合数据语义丢失、判等困难改用值对象
@Data / @Builder破坏结构判等@Getter + @Setter(PROTECTED)
值对象构造期内做字段校验校验逻辑固化在数据载体、难以按场景调整校验交由校验规则类统一执行(见 §5)

下一步