值对象
值对象(Value Object)是领域驱动设计中的重要概念。与实体不同,值对象通过其属性值来标识,而不是通过唯一标识符(Id)。
什么是值对象
Section titled “什么是值对象”值对象具有以下特征:
- 无唯一标识:没有
Id属性,通过属性值来识别 - 不可变性:一旦创建,属性值不能改变
- 值相等性:两个值对象相等当且仅当所有属性值都相同
- 可替换性:可以用另一个相同值的对象替换
- 无副作用:方法不改变对象状态,返回新对象
MiCake 提供了 ValueObject 抽象类用于定义值对象:
using MiCake.DDD.Domain;using System.Collections.Generic;
public class Money : ValueObject{ public decimal Amount { get; } public string Currency { get; }
public Money(decimal amount, string currency) { Amount = amount; Currency = currency ?? throw new ArgumentNullException(nameof(currency)); }
// 定义相等性比较的组件 protected override IEnumerable<object> GetEqualityComponents() { yield return Amount; yield return Currency; }
// 业务方法返回新对象,保持不可变性 public Money Add(Money other) { if (Currency != other.Currency) throw new DomainException("Cannot add money with different currencies");
return new Money(Amount + other.Amount, Currency); }
public Money Multiply(decimal multiplier) { return new Money(Amount * multiplier, Currency); }
public override string ToString() => $"{Amount} {Currency}";}Record 值对象
Section titled “Record 值对象”对于简单的值对象,可以使用 C# 9.0+ 的 record 类型:
using MiCake.DDD.Domain;
// 使用 record 简化值对象定义public record Address : RecordValueObject{ public string Street { get; init; } public string City { get; init; } public string ZipCode { get; init; } public string Country { get; init; }
public Address(string street, string city, string zipCode, string country) { Street = street ?? throw new ArgumentNullException(nameof(street)); City = city ?? throw new ArgumentNullException(nameof(city)); ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode)); Country = country ?? throw new ArgumentNullException(nameof(country)); }}RecordValueObject 的优势:
- 自动实现值相等性
- 自动实现 GetHashCode
- 自动实现解构
- 支持 with 表达式
- 更简洁的语法
值对象的相等性
Section titled “值对象的相等性”基于所有属性的相等性
Section titled “基于所有属性的相等性”public class Address : ValueObject{ public string Street { get; } public string City { get; } public string ZipCode { get; }
public Address(string street, string city, string zipCode) { Street = street; City = city; ZipCode = zipCode; }
// 返回用于比较的所有属性 protected override IEnumerable<object> GetEqualityComponents() { yield return Street; yield return City; yield return ZipCode; }}
// 使用示例var address1 = new Address("123 Main St", "Beijing", "100000");var address2 = new Address("123 Main St", "Beijing", "100000");var address3 = new Address("456 Park Ave", "Shanghai", "200000");
Console.WriteLine(address1 == address2); // True - 所有属性相同Console.WriteLine(address1 == address3); // False - 属性不同复杂类型的相等性
Section titled “复杂类型的相等性”public class DateRange : ValueObject{ public DateTime StartDate { get; } public DateTime EndDate { get; }
public DateRange(DateTime startDate, DateTime endDate) { if (endDate < startDate) throw new DomainException("End date must be after start date");
StartDate = startDate; EndDate = endDate; }
protected override IEnumerable<object> GetEqualityComponents() { yield return StartDate; yield return EndDate; }
public int GetDays() => (EndDate - StartDate).Days;
public bool Contains(DateTime date) { return date >= StartDate && date <= EndDate; }
public bool Overlaps(DateRange other) { return StartDate <= other.EndDate && EndDate >= other.StartDate; }}值对象的不可变性
Section titled “值对象的不可变性”正确实现不可变性
Section titled “正确实现不可变性”public class PersonName : ValueObject{ // 只读属性 public string FirstName { get; } public string LastName { get; } public string FullName => $"{FirstName} {LastName}";
public PersonName(string firstName, string lastName) { FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName)); LastName = lastName ?? throw new ArgumentNullException(nameof(lastName)); }
// 方法返回新对象,不修改当前对象 public PersonName ChangeFirstName(string newFirstName) { return new PersonName(newFirstName, LastName); }
public PersonName ChangeLastName(string newLastName) { return new PersonName(FirstName, newLastName); }
protected override IEnumerable<object> GetEqualityComponents() { yield return FirstName; yield return LastName; }}
// 使用示例var name = new PersonName("张", "三");var newName = name.ChangeFirstName("李"); // 返回新对象Console.WriteLine(name.FirstName); // 张 - 原对象未改变Console.WriteLine(newName.FirstName); // 李 - 新对象常见值对象示例
Section titled “常见值对象示例”1. 货币金额
Section titled “1. 货币金额”public class Money : ValueObject{ public decimal Amount { get; } public string Currency { get; }
public Money(decimal amount, string currency) { if (amount < 0) throw new DomainException("Amount cannot be negative");
Amount = amount; Currency = currency?.ToUpper() ?? throw new ArgumentNullException(nameof(currency)); }
public Money Add(Money other) { if (Currency != other.Currency) throw new DomainException($"Cannot add {other.Currency} to {Currency}");
return new Money(Amount + other.Amount, Currency); }
public Money Subtract(Money other) { if (Currency != other.Currency) throw new DomainException($"Cannot subtract {other.Currency} from {Currency}");
return new Money(Amount - other.Amount, Currency); }
public Money Multiply(decimal multiplier) { return new Money(Amount * multiplier, Currency); }
protected override IEnumerable<object> GetEqualityComponents() { yield return Amount; yield return Currency; }
public static Money Zero(string currency) => new Money(0, currency); public static Money CNY(decimal amount) => new Money(amount, "CNY"); public static Money USD(decimal amount) => new Money(amount, "USD");
public override string ToString() => $"{Amount:F2} {Currency}";}public class Address : ValueObject{ public string Country { get; } public string Province { get; } public string City { get; } public string Street { get; } public string ZipCode { get; }
public Address(string country, string province, string city, string street, string zipCode) { Country = country ?? throw new ArgumentNullException(nameof(country)); Province = province ?? throw new ArgumentNullException(nameof(province)); City = city ?? throw new ArgumentNullException(nameof(city)); Street = street ?? throw new ArgumentNullException(nameof(street)); ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode)); }
protected override IEnumerable<object> GetEqualityComponents() { yield return Country; yield return Province; yield return City; yield return Street; yield return ZipCode; }
public override string ToString() { return $"{Country}, {Province}, {City}, {Street}, {ZipCode}"; }}3. 邮箱地址
Section titled “3. 邮箱地址”public class EmailAddress : ValueObject{ public string Value { get; }
public EmailAddress(string value) { if (string.IsNullOrWhiteSpace(value)) throw new DomainException("Email address cannot be empty");
if (!value.Contains("@") || !value.Contains(".")) throw new DomainException("Invalid email format");
Value = value.ToLower().Trim(); }
protected override IEnumerable<object> GetEqualityComponents() { yield return Value; }
public string GetDomain() { return Value.Split('@')[1]; }
public override string ToString() => Value;
// 隐式转换 public static implicit operator string(EmailAddress email) => email.Value;}值对象在实体中的使用
Section titled “值对象在实体中的使用”作为实体属性
Section titled “作为实体属性”public class Customer : AggregateRoot<int>{ // 值对象属性 public PersonName Name { get; private set; } public EmailAddress Email { get; private set; } public PhoneNumber Phone { get; private set; } public Address ShippingAddress { get; private set; }
private Customer() { }
public static Customer Create(PersonName name, EmailAddress email) { return new Customer { Name = name, Email = email }; }
public void UpdateEmail(EmailAddress newEmail) { if (newEmail == null) throw new ArgumentNullException(nameof(newEmail));
Email = newEmail; RaiseDomainEvent(new CustomerEmailChangedEvent(Id, newEmail.Value)); }
public void UpdateShippingAddress(Address newAddress) { ShippingAddress = newAddress; }}作为方法参数
Section titled “作为方法参数”public class Order : AggregateRoot<int>{ private Money _totalAmount; private Address _shippingAddress;
public void UpdateShippingAddress(Address newAddress) { if (newAddress == null) throw new ArgumentNullException(nameof(newAddress));
_shippingAddress = newAddress; }
public void ApplyDiscount(Percentage discountRate) { var discount = discountRate.ApplyTo(_totalAmount); _totalAmount = _totalAmount.Subtract(discount);
RaiseDomainEvent(new DiscountAppliedEvent(Id, discount)); }}值对象的持久化
Section titled “值对象的持久化”EF Core 配置
Section titled “EF Core 配置”public class CustomerConfiguration : IEntityTypeConfiguration<Customer>{ public void Configure(EntityTypeBuilder<Customer> builder) { // 方式一:拆分为多个列 builder.OwnsOne(c => c.Name, name => { name.Property(n => n.FirstName) .HasColumnName("FirstName") .HasMaxLength(50); name.Property(n => n.LastName) .HasColumnName("LastName") .HasMaxLength(50); });
// 方式二:使用 JSON 存储 builder.OwnsOne(c => c.Address, address => { address.ToJson(); });
// 方式三:使用转换器 builder.Property(c => c.Email) .HasConversion( email => email.Value, value => new EmailAddress(value) ); }}值对象 vs 实体
Section titled “值对象 vs 实体”| 特性 | 值对象 | 实体 |
|---|---|---|
| 标识 | 无唯一标识 | 有唯一 Id |
| 相等性 | 基于属性值 | 基于 Id |
| 可变性 | 不可变 | 可变 |
| 生命周期 | 无独立生命周期 | 有独立生命周期 |
| 可替换性 | 可以被相同值的对象替换 | 不能被替换 |
使用值对象:
- 描述事物的属性或度量
- 不需要追踪变更历史
- 可以被相同值的对象替换
- 例如:金额、地址、日期范围、邮箱
使用实体:
- 需要唯一标识
- 需要追踪变更历史
- 有独立的生命周期
- 例如:用户、订单、商品
1. 保持值对象简单
Section titled “1. 保持值对象简单”// ✅ 好的做法 - 简单明了public class Temperature : ValueObject{ public decimal Value { get; } public string Unit { get; }
public Temperature(decimal value, string unit) { Value = value; Unit = unit; }
protected override IEnumerable<object> GetEqualityComponents() { yield return Value; yield return Unit; }}
// ❌ 避免 - 过于复杂public class ComplexValue : ValueObject{ // 包含太多属性和复杂逻辑 // 可能应该拆分为多个值对象或变成实体}2. 在构造函数中验证
Section titled “2. 在构造函数中验证”public class Age : ValueObject{ public int Value { get; }
public Age(int value) { if (value < 0) throw new DomainException("Age cannot be negative"); if (value > 150) throw new DomainException("Age seems unrealistic");
Value = value; }
protected override IEnumerable<object> GetEqualityComponents() { yield return Value; }}3. 提供工厂方法
Section titled “3. 提供工厂方法”public class Money : ValueObject{ public decimal Amount { get; } public string Currency { get; }
private Money(decimal amount, string currency) { Amount = amount; Currency = currency; }
// 工厂方法 public static Money Create(decimal amount, string currency) { if (amount < 0) throw new DomainException("Amount cannot be negative"); return new Money(amount, currency); }
public static Money Zero(string currency) => new Money(0, currency); public static Money CNY(decimal amount) => Create(amount, "CNY"); public static Money USD(decimal amount) => Create(amount, "USD");
protected override IEnumerable<object> GetEqualityComponents() { yield return Amount; yield return Currency; }}4. 实现有意义的方法
Section titled “4. 实现有意义的方法”public class DateRange : ValueObject{ public DateTime StartDate { get; } public DateTime EndDate { get; }
public DateRange(DateTime startDate, DateTime endDate) { if (endDate < startDate) throw new DomainException("End date must be after start date");
StartDate = startDate; EndDate = endDate; }
// 有业务含义的方法 public int GetDurationInDays() => (EndDate - StartDate).Days;
public bool Contains(DateTime date) => date >= StartDate && date <= EndDate;
public bool Overlaps(DateRange other) => StartDate <= other.EndDate && EndDate >= other.StartDate;
public DateRange ExtendBy(int days) => new DateRange(StartDate, EndDate.AddDays(days));
protected override IEnumerable<object> GetEqualityComponents() { yield return StartDate; yield return EndDate; }}❌ 值对象包含可变状态
Section titled “❌ 值对象包含可变状态”// 错误:属性可以被修改public class Address : ValueObject{ public string Street { get; set; } // 不要用 set public string City { get; set; }}✅ 正确的不可变实现
Section titled “✅ 正确的不可变实现”public class Address : ValueObject{ public string Street { get; } // 只读 public string City { get; }
public Address(string street, string city) { Street = street; City = city; }
protected override IEnumerable<object> GetEqualityComponents() { yield return Street; yield return City; }}值对象是 DDD 中的重要概念,在 MiCake 中:
- 继承
ValueObject基类或RecordValueObject - 通过属性值比较相等性
- 保持不可变性
- 在实体中作为属性使用
- 封装领域概念和业务规则
- 使代码更具表达力和类型安全
下一步: