--- title: 值对象 description: 领域驱动设计中的重要概念,通过属性值来标识的不可变对象 --- 值对象(Value Object)是领域驱动设计中的重要概念。与实体不同,值对象通过其属性值来标识,而不是通过唯一标识符(Id)。 ## 什么是值对象 值对象具有以下特征: 1. **无唯一标识**:没有 `Id` 属性,通过属性值来识别 2. **不可变性**:一旦创建,属性值不能改变 3. **值相等性**:两个值对象相等当且仅当所有属性值都相同 4. **可替换性**:可以用另一个相同值的对象替换 5. **无副作用**:方法不改变对象状态,返回新对象 ## 值对象基类 MiCake 提供了 `ValueObject` 抽象类用于定义值对象: ```csharp 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 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 值对象 对于简单的值对象,可以使用 C# 9.0+ 的 record 类型: ```csharp 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 表达式 - 更简洁的语法 ## 值对象的相等性 ### 基于所有属性的相等性 ```csharp 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 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 - 属性不同 ``` ### 复杂类型的相等性 ```csharp 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 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; } } ``` ## 值对象的不可变性 ### 正确实现不可变性 ```csharp 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 GetEqualityComponents() { yield return FirstName; yield return LastName; } } // 使用示例 var name = new PersonName("张", "三"); var newName = name.ChangeFirstName("李"); // 返回新对象 Console.WriteLine(name.FirstName); // 张 - 原对象未改变 Console.WriteLine(newName.FirstName); // 李 - 新对象 ``` ## 常见值对象示例 ### 1. 货币金额 ```csharp 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 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}"; } ``` ### 2. 地址 ```csharp 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 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. 邮箱地址 ```csharp 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 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; } ``` ## 值对象在实体中的使用 ### 作为实体属性 ```csharp public class Customer : AggregateRoot { // 值对象属性 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; } } ``` ### 作为方法参数 ```csharp public class Order : AggregateRoot { 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)); } } ``` ## 值对象的持久化 ### EF Core 配置 ```csharp public class CustomerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder 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 实体 | 特性 | 值对象 | 实体 | |------|--------|------| | 标识 | 无唯一标识 | 有唯一 Id | | 相等性 | 基于属性值 | 基于 Id | | 可变性 | 不可变 | 可变 | | 生命周期 | 无独立生命周期 | 有独立生命周期 | | 可替换性 | 可以被相同值的对象替换 | 不能被替换 | ### 如何选择 **使用值对象**: - 描述事物的属性或度量 - 不需要追踪变更历史 - 可以被相同值的对象替换 - 例如:金额、地址、日期范围、邮箱 **使用实体**: - 需要唯一标识 - 需要追踪变更历史 - 有独立的生命周期 - 例如:用户、订单、商品 ## 最佳实践 ### 1. 保持值对象简单 ```csharp // ✅ 好的做法 - 简单明了 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 GetEqualityComponents() { yield return Value; yield return Unit; } } // ❌ 避免 - 过于复杂 public class ComplexValue : ValueObject { // 包含太多属性和复杂逻辑 // 可能应该拆分为多个值对象或变成实体 } ``` ### 2. 在构造函数中验证 ```csharp 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 GetEqualityComponents() { yield return Value; } } ``` ### 3. 提供工厂方法 ```csharp 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 GetEqualityComponents() { yield return Amount; yield return Currency; } } ``` ### 4. 实现有意义的方法 ```csharp 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 GetEqualityComponents() { yield return StartDate; yield return EndDate; } } ``` ## 常见错误 ### ❌ 值对象包含可变状态 ```csharp // 错误:属性可以被修改 public class Address : ValueObject { public string Street { get; set; } // 不要用 set public string City { get; set; } } ``` ### ✅ 正确的不可变实现 ```csharp 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 GetEqualityComponents() { yield return Street; yield return City; } } ``` ## 小结 值对象是 DDD 中的重要概念,在 MiCake 中: - 继承 `ValueObject` 基类或 `RecordValueObject` - 通过属性值比较相等性 - 保持不可变性 - 在实体中作为属性使用 - 封装领域概念和业务规则 - 使代码更具表达力和类型安全 下一步: - 学习[聚合根](/domain-driven/aggregate-root/)了解聚合设计 - 阅读[仓储](/domain-driven/repository/)理解持久化 - 查看[实体](/domain-driven/entity/)对比实体与值对象