欢迎光临
我们一直在努力

DDD-010:值对象(Value Object)

DDD-010:值对象(Value Object)

10.1 值对象的定义与特征

10.1.1 什么是值对象?

【原理】 值对象(Value Object)是领域模型中通过属性值而非身份标识来定义的对象。与实体不同,值对象没有唯一标识符,两个值对象只要所有属性相同,就被认为是相等的。

值对象的核心特征:

  • 无标识符:不需要唯一ID来区分
  • 不可变性(Immutability):创建后状态不可改变
  • 值相等性:通过属性值判断相等
  • 可替换性:需要变更时整体替换

【历史架构问题】

// ❌ 使用基本类型表示业务概念
public class Order {


private Long id;
private BigDecimal amount; // 金额:缺失货币单位
private String currency; // 货币:分散管理
private String country; // 地址:散落的字段
private String province;
private String city;
private String street;
private String zipCode;
private String email; // 邮箱:无格式验证
private String phone; // 手机号:无格式验证
}

// 问题1:金额计算缺乏类型安全
BigDecimal amount1 = new BigDecimal(\”100.00\”);
BigDecimal amount2 = new BigDecimal(\”200.00\”);
String currency1 = \”CNY\”;
String currency2 = \”USD\”;
amount1.add(amount2); // 编译通过!不同货币相加,运行时错误

// 问题2:地址验证散落各处
public void validateAddress(String country, String province,
String city, String street, String zipCode) {


// 每个使用地址的地方都要重复验证
}

// 问题3:邮箱格式无法保证
order.setEmail(\”invalid-email\”); // 可以设置无效邮箱

存在问题:

  • ❌ 类型不安全:基本类型无法表达业务约束
  • ❌ 验证分散:相同的验证逻辑到处重复
  • ❌ 概念模糊:金额、地址等概念被拆成基本类型
  • ❌ 操作不安全:不同货币可以直接相加
  • 【DDD 如何解决】

    // ✅ 使用值对象封装业务概念
    public class Order extends Entity<OrderId> {


    private OrderId id;
    private Money totalAmount; // 值对象:金额+货币
    private Address shippingAddress; // 值对象:完整地址
    private EmailAddress contactEmail; // 值对象:邮箱(带验证)
    private PhoneNumber contactPhone; // 值对象:手机号(带验证)
    }

    // 类型安全的金额计算
    Money cny100 = Money.of(new BigDecimal(\”100\”), \”CNY\”);
    Money cny200 = Money.of(new BigDecimal(\”200\”), \”CNY\”);
    Money usd50 = Money.of(new BigDecimal(\”50\”), \”USD\”);

    cny100.add(cny200); // ✅ CNY 300.00
    cny100.add(usd50); // ❌ 抛出异常:货币不同,不能相加

    // 地址验证内置
    Address address = Address.of(\”中国\”, \”上海市\”, \”上海市\”, \”南京路100号\”, \”200000\”);
    // 创建时即验证,后续使用无需重复验证

    // 邮箱格式保证
    EmailAddress email = EmailAddress.of(\”user@example.com\”); // ✅
    EmailAddress invalid = EmailAddress.of(\”not-an-email\”); // ❌ 抛出异常

    【设计优势】

    对比维度
    基本类型
    值对象
    类型安全 无(Long可混用) 强(Money vs Long)
    验证逻辑 分散在调用方 集中在值对象内
    业务表达 模糊(BigDecimal) 清晰(Money)
    不可变性 无保证 强制保证
    自文档化

    10.1.2 不可变性(Immutability)

    【原理】 值对象必须不可变。一旦创建,其内部状态不可改变。需要修改时,创建新的值对象实例。

    为什么不可变?

  • 线程安全:无需同步即可在多线程间共享
  • 引用安全:传递给其他对象不会被意外修改
  • 哈希稳定:hashCode不变,可安全用于Map键和Set元素
  • 语义清晰:修改操作返回新对象,意图明确
  • 【历史架构问题】

    // ❌ 可变的\”值\”对象
    public class Money {


    private BigDecimal amount;
    private String currency;

    public void setAmount(BigDecimal amount) {


    this.amount = amount; // 可被修改!
    }

    public void add(Money other) {


    this.amount = this.amount.add(other.amount); // 修改自身!
    }
    }

    // 问题1:传递后被意外修改
    Money price = new Money(new BigDecimal(\”100\”), \”CNY\”);
    Money total = price;
    total.add(new Money(new BigDecimal(\”50\”), \”CNY\”));
    // price也变了!Java引用传递

    // 问题2:Map键变化
    Map<Money, String> map = new HashMap<>();
    Money key = new Money(new BigDecimal(\”100\”), \”CNY\”);
    map.put(key, \”订单1\”);
    key.setAmount(new BigDecimal(\”200\”)); // 键变了!
    map.get(key); // 可能找不到

    【DDD 如何解决】

    // ✅ 不可变值对象
    public final class Money {


    private final BigDecimal amount;
    private final Currency currency;

    // 构造函数:所有字段final
    public Money(BigDecimal amount, Currency currency) {


    this.amount = amount;
    this.currency = currency;
    }

    // 没有 setter

    // 操作返回新对象
    public Money add(Money other) {


    if (!this.currency.equals(other.currency)) {


    throw new CurrencyMismatchException(
    \”不能对不同货币执行加法: \” + currency + \” vs \” + other.currency
    );
    }
    return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money subtract(Money other) {


    if (!this.currency.equals(other.currency)) {


    throw new CurrencyMismatchException(
    \”不能对不同货币执行减法\”
    );
    }
    return new Money(this.amount.subtract(other.amount), this.currency);
    }

    public Money multiply(int multiplier) {


    return new Money(this.amount.multiply(BigDecimal.valueOf(multiplier)), this.currency);
    }

    public Money negate() {


    return new Money(this.amount.negate(), this.currency);
    }

    public boolean isGreaterThan(Money other) {


    ensureSameCurrency(other);
    return this.amount.compareTo(other.amount) > 0;
    }

    public boolean isZero() {


    return this.amount.compareTo(BigDecimal.ZERO) == 0;
    }

    public boolean isNegative() {


    return this.amount.compareTo(BigDecimal.ZERO) < 0;
    }

    // 静态工厂方法
    public static Money of(BigDecimal amount, String currencyCode) {


    return new Money(amount, Currency.getInstance(currencyCode));
    }

    public static Money zero(String currencyCode) {


    return new Money(BigDecimal.ZERO, Currency.getInstance(currencyCode));
    }

    public static Money CNY(BigDecimal amount) {


    return new Money(amount, Currency.getInstance(\”CNY\”));
    }

    public static Money USD(BigDecimal amount) {


    return new Money(amount, Currency.getInstance(\”USD\”));
    }

    private void ensureSameCurrency(Money other) {


    if (!this.currency.equals(other.currency)) {


    throw new CurrencyMismatchException(\”货币不匹配\”);
    }
    }

    // Getter
    public BigDecimal getAmount() {

    return amount; }
    public Currency getCurrency() {

    return currency; }

    // 基于值的equals和hashCode
    @Override
    public boolean equals(Object o) {


    if (this == o) return true;
    if (!(o instanceof Money)) return false;
    Money money = (Money) o;
    return amount.compareTo(money.amount) == 0
    && currency.equals(money.currency);
    }

    @Override
    public int hashCode() {


    return Objects.hash(amount.stripTrailingZeros(), currency);
    }

    @Override
    public String toString() {


    return currency.getCurrencyCode() + \” \” + amount;
    }
    }

    // 使用示例
    Money price = Money.CNY(new BigDecimal(\”100\”));
    Money shipping = Money.CNY(new BigDecimal(\”10\”));
    Money total = price.add(shipping);

    赞(0)
    未经允许不得转载:171主机测评 » DDD-010:值对象(Value Object)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址