--- title: 实体 description: 领域驱动设计中的核心概念之一,具有唯一标识的领域对象 --- 实体(Entity)是领域驱动设计中的核心概念之一。在 MiCake 中,实体是具有唯一标识的领域对象,通过 `Id` 进行识别和比较。 ## 什么是实体 实体具有以下特征: 1. **唯一标识**:每个实体都有一个唯一的 `Id` 属性 2. **可变性**:实体的属性值可以改变,但标识不变 3. **身份相等**:两个实体相等当且仅当它们的 `Id` 相同 4. **生命周期**:实体有明确的生命周期,从创建到删除 ## 实体基类 MiCake 提供了 `Entity` 基类用于定义实体: ```csharp using MiCake.DDD.Domain; // 使用自定义 Key 类型 public class Customer : Entity { public string Name { get; private set; } public string Email { get; private set; } public DateTime RegisterDate { get; private set; } // 构造函数 private Customer() { } // EF Core 需要 // 在MiCake中推荐使用Create静态方法来创建领域对象 public static Customer Create(Guid id, string name, string email) { return new Customer { Id = id, Name = name, Email = email, RegisterDate = DateTime.UtcNow }; } } ``` 对于使用 `int` 类型作为主键的实体,可以使用简化版本: ```csharp // 默认使用 int 作为 Key 类型 public class Product : Entity { public string Name { get; private set; } public decimal Price { get; private set; } public int Stock { get; private set; } private Product() { } public Product(string name, decimal price, int stock) { Name = name; Price = price; Stock = stock; } } ``` ## 实体标识(Identity) ### Id 属性 实体的 `Id` 属性是其唯一标识: ```csharp public abstract class Entity : IEntity where TKey : notnull { public virtual TKey Id { get; init; } = default!; } ``` 特点: - 使用 `init` 访问器,确保 Id 在初始化后不可变 - 可以在构造函数或对象初始化器中设置 - 支持任何非空类型作为 Key ### 常用 Key 类型 ```csharp // int 类型(最常用) public class Order : Entity { } // Guid 类型(分布式系统推荐) public class Customer : Entity { } // long 类型(大数据量场景) public class LogEntry : Entity { } // string 类型(业务编号) public class Invoice : Entity { } ``` ### 生成 Id 的时机 ```csharp // 方式一:工厂方法生成 (推荐) public class Product : Entity { private Product() { } public static Product Create(string name, decimal price) { return new Product { Id = Guid.NewGuid(), Name = name, Price = price }; } } // 方式二:构造时生成 public class Customer : Entity { public Customer(string name) { Id = Guid.NewGuid(); // 在构造函数中生成 Name = name; } } // 方式二:数据库自增 public class Order : Entity { // Id 由数据库自增生成,无需手动设置 public Order(Customer customer) { Customer = customer; } } ``` ## 实体相等性 ### 相等性比较规则 MiCake 实体基类实现了基于标识的相等性比较: ```csharp var customer1 = new Customer(Guid.NewGuid(), "张三", "zhang@example.com"); var customer2 = new Customer(customer1.Id, "李四", "li@example.com"); // 只要 Id 相同,就认为是同一个实体 bool areEqual = customer1 == customer2; // true // 即使属性值不同,但 Id 相同,仍然相等 Console.WriteLine(customer1.Name); // 张三 Console.WriteLine(customer2.Name); // 李四 Console.WriteLine(customer1 == customer2); // true ``` ## 领域事件 实体可以触发领域事件,用于捕获业务中发生的重要事件。 ### 触发领域事件 ```csharp public class Order : Entity { public OrderStatus Status { get; private set; } public List Items { get; private set; } = new(); public void AddItem(Product product, int quantity) { var item = new OrderItem(product, quantity); Items.Add(item); // 触发领域事件 RaiseDomainEvent(new OrderItemAddedEvent(Id, product.Id, quantity)); } public void Submit() { if (Status != OrderStatus.Draft) throw new DomainException("Only draft orders can be submitted"); Status = OrderStatus.Submitted; // 触发提交事件 RaiseDomainEvent(new OrderSubmittedEvent(Id, Items.Sum(i => i.TotalPrice))); } } ``` ### 访问领域事件 ```csharp // 获取实体的所有待处理事件 IReadOnlyCollection events = order.DomainEvents; // 清除所有事件(通常由框架自动调用) order.ClearDomainEvents(); ``` 事件会在工作单元提交(`IUnitOfWork.CommitAsync()`)时自动派发,详见[领域事件](/domain-driven/domain-event/)章节。 ## 实体的业务方法 ### 封装业务逻辑 实体应该包含相关的业务逻辑,而不仅仅是数据容器: ```csharp public class BankAccount : Entity { public decimal Balance { get; private set; } public AccountStatus Status { get; private set; } private BankAccount() { } public static BankAccount Open(Guid id, decimal initialDeposit) { if (initialDeposit < 0) throw new DomainException("Initial deposit cannot be negative"); var account = new BankAccount { Id = id, Balance = initialDeposit, Status = AccountStatus.Active }; account.RaiseDomainEvent(new AccountOpenedEvent(id, initialDeposit)); return account; } public void Deposit(decimal amount) { if (amount <= 0) throw new DomainException("Deposit amount must be positive"); if (Status != AccountStatus.Active) throw new DomainException("Account is not active"); Balance += amount; RaiseDomainEvent(new MoneyDepositedEvent(Id, amount, Balance)); } public void Withdraw(decimal amount) { if (amount <= 0) throw new DomainException("Withdrawal amount must be positive"); if (Status != AccountStatus.Active) throw new DomainException("Account is not active"); if (Balance < amount) throw new DomainException("Insufficient balance"); Balance -= amount; RaiseDomainEvent(new MoneyWithdrawnEvent(Id, amount, Balance)); } public void Close() { if (Balance != 0) throw new DomainException("Cannot close account with non-zero balance"); Status = AccountStatus.Closed; RaiseDomainEvent(new AccountClosedEvent(Id)); } } ``` ### 方法设计原则 1. **使用私有 setter**:防止外部直接修改属性 2. **验证业务规则**:在方法中验证业务约束 3. **抛出领域异常**:使用 `DomainException` 报告业务错误 4. **触发领域事件(可选)**:记录重要的业务事件 5. **保持状态一致**:确保实体在方法执行后处于有效状态 ## 实体 vs 聚合根 ### 普通实体 普通实体没有独立的生命周期,必须属于某个聚合: ```csharp // OrderItem 是普通实体,不是聚合根 public class OrderItem : Entity { public int OrderId { get; private set; } public int ProductId { get; private set; } public int Quantity { get; private set; } public decimal Price { get; private set; } // 只能通过 Order 聚合根访问和修改 } ``` ### 聚合根 聚合根是特殊的实体,可以独立存在并作为聚合的入口: ```csharp // Order 是聚合根 public class Order : AggregateRoot { private List _items = new(); public IReadOnlyCollection Items => _items.AsReadOnly(); public void AddItem(Product product, int quantity, decimal price) { var item = new OrderItem { ProductId = product.Id, Quantity = quantity, Price = price }; _items.Add(item); } // 所有对 OrderItem 的操作都通过 Order } ``` 关键区别: - 普通实体:`Entity` - 聚合根:`AggregateRoot`(继承自 `Entity`) - 只有聚合根可以有仓储 - 普通实体只能通过聚合根访问 详见[聚合根](/domain-driven/aggregate-root/)章节。 ## 最佳实践 ### 1. 使用工厂方法创建实体 ```csharp public class Customer : Entity { private Customer() { } // 工厂方法确保实体创建时满足业务规则 public static Customer Create(string name, string email) { if (string.IsNullOrWhiteSpace(name)) throw new DomainException("Customer name is required"); if (!email.Contains("@")) throw new DomainException("Invalid email format"); return new Customer { Id = Guid.NewGuid(), Name = name, Email = email, RegisterDate = DateTime.UtcNow }; } } ``` ### 2. 保持状态一致性 ```csharp public class Order : Entity { private List _items = new(); private decimal _totalAmount; public void AddItem(OrderItem item) { _items.Add(item); // 立即更新总金额,保持一致性 _totalAmount += item.TotalPrice; } public void RemoveItem(OrderItem item) { if (_items.Remove(item)) { _totalAmount -= item.TotalPrice; } } } ``` ### 3. 使用私有 setter 保护数据 ```csharp public class Product : Entity { // 使用 private set 防止外部修改 public string Name { get; private set; } public decimal Price { get; private set; } // 通过方法修改,可以加入验证逻辑 public void UpdatePrice(decimal newPrice) { if (newPrice < 0) throw new DomainException("Price cannot be negative"); Price = newPrice; } } ``` ### 4. 验证业务规则 ```csharp public class ShoppingCart : Entity { private List _items = new(); private const int MaxItemsCount = 100; public void AddItem(Product product, int quantity) { // 业务规则验证 if (quantity <= 0) throw new DomainException("Quantity must be positive"); if (_items.Count >= MaxItemsCount) throw new DomainException("Cart is full"); if (product.Stock < quantity) throw new DomainException("Insufficient stock"); // 执行操作 _items.Add(new CartItem(product, quantity)); } } ``` ### 5. 合理使用领域事件 ```csharp public class User : Entity { public string Email { get; private set; } public bool IsEmailVerified { get; private set; } public void VerifyEmail(string verificationCode) { // 验证逻辑 if (IsEmailVerified) throw new DomainException("Email already verified"); // 验证 code... IsEmailVerified = true; // 触发事件,让其他部分响应(如发送欢迎邮件) RaiseDomainEvent(new EmailVerifiedEvent(Id, Email)); } } ``` ## 常见错误 ### ❌ 贫血模型 ```csharp // 错误:实体只有数据,没有行为 public class Order : Entity { public int CustomerId { get; set; } public OrderStatus Status { get; set; } public decimal TotalAmount { get; set; } } // 业务逻辑在外部服务中 public class OrderService { public void SubmitOrder(Order order) { order.Status = OrderStatus.Submitted; // ... } } ``` ### ✅ 充血模型 ```csharp // 正确:实体包含业务逻辑 public class Order : Entity { public int CustomerId { get; private set; } public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; } public void Submit() { if (Status != OrderStatus.Draft) throw new DomainException("Only draft orders can be submitted"); Status = OrderStatus.Submitted; RaiseDomainEvent(new OrderSubmittedEvent(Id)); } } ``` ## 小结 实体是 DDD 中的基础概念,在 MiCake 中: - 继承 `Entity` 或 `Entity` 基类 - 通过唯一的 `Id` 标识 - 可以触发领域事件 - 应该包含相关的业务逻辑 - 使用私有 setter 保护数据 - 通过方法修改状态并验证业务规则 下一步: - 学习[值对象](/domain-driven/value-object/)了解不可变对象 - 阅读[聚合根](/domain-driven/aggregate-root/)理解聚合设计 - 查看[领域事件](/domain-driven/domain-event/)掌握事件驱动