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)
【原理】 值对象必须不可变。一旦创建,其内部状态不可改变。需要修改时,创建新的值对象实例。
为什么不可变?
【历史架构问题】
// ❌ 可变的\”值\”对象
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);




