实体
实体(Entity)是领域驱动设计中的核心概念之一。在 MiCake 中,实体是具有唯一标识的领域对象,通过 Id 进行识别和比较。
实体具有以下特征:
- 唯一标识:每个实体都有一个唯一的
Id属性 - 可变性:实体的属性值可以改变,但标识不变
- 身份相等:两个实体相等当且仅当它们的
Id相同 - 生命周期:实体有明确的生命周期,从创建到删除
MiCake 提供了 Entity<TKey> 基类用于定义实体:
using MiCake.DDD.Domain;
// 使用自定义 Key 类型public class Customer : Entity<Guid>{ 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 类型作为主键的实体,可以使用简化版本:
// 默认使用 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)
Section titled “实体标识(Identity)”实体的 Id 属性是其唯一标识:
public abstract class Entity<TKey> : IEntity<TKey> where TKey : notnull{ public virtual TKey Id { get; init; } = default!;}特点:
- 使用
init访问器,确保 Id 在初始化后不可变 - 可以在构造函数或对象初始化器中设置
- 支持任何非空类型作为 Key
常用 Key 类型
Section titled “常用 Key 类型”// int 类型(最常用)public class Order : Entity<int> { }
// Guid 类型(分布式系统推荐)public class Customer : Entity<Guid> { }
// long 类型(大数据量场景)public class LogEntry : Entity<long> { }
// string 类型(业务编号)public class Invoice : Entity<string> { }生成 Id 的时机
Section titled “生成 Id 的时机”// 方式一:工厂方法生成 (推荐)public class Product : Entity<Guid>{ private Product() { }
public static Product Create(string name, decimal price) { return new Product { Id = Guid.NewGuid(), Name = name, Price = price }; }}
// 方式二:构造时生成public class Customer : Entity<Guid>{ public Customer(string name) { Id = Guid.NewGuid(); // 在构造函数中生成 Name = name; }}
// 方式二:数据库自增public class Order : Entity<int>{ // Id 由数据库自增生成,无需手动设置 public Order(Customer customer) { Customer = customer; }}相等性比较规则
Section titled “相等性比较规则”MiCake 实体基类实现了基于标识的相等性比较:
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实体可以触发领域事件,用于捕获业务中发生的重要事件。
触发领域事件
Section titled “触发领域事件”public class Order : Entity<int>{ public OrderStatus Status { get; private set; } public List<OrderItem> 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))); }}访问领域事件
Section titled “访问领域事件”// 获取实体的所有待处理事件IReadOnlyCollection<IDomainEvent> events = order.DomainEvents;
// 清除所有事件(通常由框架自动调用)order.ClearDomainEvents();事件会在调用 SaveChangesAsync() 时自动派发,详见领域事件章节。
实体的业务方法
Section titled “实体的业务方法”封装业务逻辑
Section titled “封装业务逻辑”实体应该包含相关的业务逻辑,而不仅仅是数据容器:
public class BankAccount : Entity<Guid>{ 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)); }}方法设计原则
Section titled “方法设计原则”- 使用私有 setter:防止外部直接修改属性
- 验证业务规则:在方法中验证业务约束
- 抛出领域异常:使用
DomainException报告业务错误 - 触发领域事件(可选):记录重要的业务事件
- 保持状态一致:确保实体在方法执行后处于有效状态
实体 vs 聚合根
Section titled “实体 vs 聚合根”普通实体没有独立的生命周期,必须属于某个聚合:
// OrderItem 是普通实体,不是聚合根public class OrderItem : Entity<int>{ 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 聚合根访问和修改}聚合根是特殊的实体,可以独立存在并作为聚合的入口:
// Order 是聚合根public class Order : AggregateRoot<int>{ private List<OrderItem> _items = new(); public IReadOnlyCollection<OrderItem> 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<TKey> - 聚合根:
AggregateRoot<TKey>(继承自Entity<TKey>) - 只有聚合根可以有仓储
- 普通实体只能通过聚合根访问
详见聚合根章节。
1. 使用工厂方法创建实体
Section titled “1. 使用工厂方法创建实体”public class Customer : Entity<Guid>{ 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. 保持状态一致性
Section titled “2. 保持状态一致性”public class Order : Entity<int>{ private List<OrderItem> _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 保护数据
Section titled “3. 使用私有 setter 保护数据”public class Product : Entity<int>{ // 使用 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. 验证业务规则
Section titled “4. 验证业务规则”public class ShoppingCart : Entity<Guid>{ private List<CartItem> _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. 合理使用领域事件
Section titled “5. 合理使用领域事件”public class User : Entity<int>{ 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)); }}❌ 贫血模型
Section titled “❌ 贫血模型”// 错误:实体只有数据,没有行为public class Order : Entity<int>{ 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; // ... }}✅ 充血模型
Section titled “✅ 充血模型”// 正确:实体包含业务逻辑public class Order : Entity<int>{ 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<TKey>或Entity基类 - 通过唯一的
Id标识 - 可以触发领域事件
- 应该包含相关的业务逻辑
- 使用私有 setter 保护数据
- 通过方法修改状态并验证业务规则
下一步: