聚合根
聚合根是聚合的根实体,是仓储操作和事务边界的入口点。它负责维护聚合内所有对象的一致性。
什么是聚合根?
Section titled “什么是聚合根?”在 DDD 中,聚合(Aggregate)是一组相关对象的集合,这些对象作为一个整体来维护业务规则的一致性。聚合根是聚合的根实体,外部对聚合的所有访问都必须通过聚合根进行。
核心特点:
- 聚合的唯一对外接口
- 事务边界
- 负责维护聚合内的不变量
- 仓储只能操作聚合根
在 MiCake 中,聚合根需要继承 AggregateRoot<TKey> 基类:
使用整数 ID
Section titled “使用整数 ID”using MiCake.DDD.Domain;using System.Collections.Generic;using System.Linq;
public class Order : AggregateRoot<int>{ private readonly List<OrderItem> _items = new();
public string CustomerName { get; private set; } public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; }
// 只暴露只读集合 public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
// 私有构造函数 private Order() { }
// 工厂方法 public static Order Create(string customerName) { var order = new Order { CustomerName = customerName, Status = OrderStatus.Draft, TotalAmount = 0 };
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id, customerName)); return order; }
// 聚合根负责管理聚合内的对象 public void AddItem(int productId, string productName, decimal price, int quantity) { if (Status != OrderStatus.Draft) throw new DomainException("只能向草稿状态的订单添加商品");
var item = new OrderItem(productId, productName, price, quantity); _items.Add(item);
RecalculateTotalAmount(); RaiseDomainEvent(new OrderItemAddedEvent(Id, productId, quantity)); }
public void RemoveItem(int productId) { if (Status != OrderStatus.Draft) throw new DomainException("只能从草稿状态的订单中移除商品");
var item = _items.FirstOrDefault(x => x.ProductId == productId); if (item != null) { _items.Remove(item); RecalculateTotalAmount(); RaiseDomainEvent(new OrderItemRemovedEvent(Id, productId)); } }
public void Confirm() { if (Status != OrderStatus.Draft) throw new DomainException("只能确认草稿状态的订单");
if (!_items.Any()) throw new DomainException("订单至少需要一个商品项");
Status = OrderStatus.Confirmed; RaiseDomainEvent(new OrderConfirmedEvent(Id, TotalAmount)); }
private void RecalculateTotalAmount() { TotalAmount = _items.Sum(item => item.Price * item.Quantity); }}
// 聚合内的实体public class OrderItem : Entity<int>{ public int ProductId { get; private set; } public string ProductName { get; private set; } public decimal Price { get; private set; } public int Quantity { get; private set; }
private OrderItem() { }
internal OrderItem(int productId, string productName, decimal price, int quantity) { ProductId = productId; ProductName = productName; Price = price; Quantity = quantity; }}
public enum OrderStatus{ Draft, Confirmed, Paid, Shipped, Completed, Cancelled}使用默认整数 ID
Section titled “使用默认整数 ID”public class Product : AggregateRoot // 等同于 AggregateRoot<int>{ public string Name { get; private set; } public decimal Price { get; private set; } public int Stock { get; private set; }}使用其他类型的 ID
Section titled “使用其他类型的 ID”// 使用 GUIDpublic class Customer : AggregateRoot<Guid>{ public string Name { get; private set; } public string Email { get; private set; }}
// 使用字符串public class Tenant : AggregateRoot<string>{ public string Name { get; private set; } public bool IsActive { get; private set; }}1. 聚合内对象的封装
Section titled “1. 聚合内对象的封装”聚合根应该封装聚合内的所有对象,外部不能直接修改它们:
public class Order : AggregateRoot<int>{ private readonly List<OrderItem> _items = new();
// ✅ 正确:返回只读集合 public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
// ✅ 正确:通过聚合根的方法修改聚合内对象 public void AddItem(int productId, string productName, decimal price, int quantity) { var item = new OrderItem(productId, productName, price, quantity); _items.Add(item); RecalculateTotalAmount(); }
public void UpdateItemQuantity(int productId, int newQuantity) { var item = _items.FirstOrDefault(x => x.ProductId == productId); if (item == null) throw new DomainException("订单项不存在");
// 通过聚合根更新 item.UpdateQuantity(newQuantity); RecalculateTotalAmount(); }}
// ❌ 错误:暴露可修改的集合public class Order : AggregateRoot<int>{ public List<OrderItem> Items { get; set; } // 外部可以直接修改}2. 维护不变量
Section titled “2. 维护不变量”聚合根负责维护聚合内的业务规则(不变量):
public class ShoppingCart : AggregateRoot<int>{ private readonly List<CartItem> _items = new(); private const int MaxItemCount = 100; private const decimal MaxTotalAmount = 50000m;
public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly(); public decimal TotalAmount { get; private set; }
public void AddItem(int productId, string productName, decimal price, int quantity) { // 不变量 1:购物车商品数量限制 if (_items.Count >= MaxItemCount) throw new DomainException($"购物车最多只能添加 {MaxItemCount} 个商品");
// 不变量 2:购物车总金额限制 var newTotalAmount = TotalAmount + (price * quantity); if (newTotalAmount > MaxTotalAmount) throw new DomainException($"购物车总金额不能超过 {MaxTotalAmount}");
// 不变量 3:每个商品只能添加一次 if (_items.Any(x => x.ProductId == productId)) throw new DomainException("该商品已在购物车中");
var item = new CartItem(productId, productName, price, quantity); _items.Add(item); TotalAmount = newTotalAmount; }}3. 领域事件
Section titled “3. 领域事件”聚合根可以触发领域事件来通知领域中发生的重要变化:
public class Order : AggregateRoot<int>{ public OrderStatus Status { get; private set; }
public void Confirm() { Status = OrderStatus.Confirmed; RaiseDomainEvent(new OrderConfirmedEvent(Id)); }
public void Pay(decimal amount, string paymentMethod) { if (Status != OrderStatus.Confirmed) throw new DomainException("只能支付已确认的订单");
if (amount != TotalAmount) throw new DomainException("支付金额不正确");
Status = OrderStatus.Paid; RaiseDomainEvent(new OrderPaidEvent(Id, amount, paymentMethod)); }
public void Ship(string trackingNumber) { if (Status != OrderStatus.Paid) throw new DomainException("只能发货已支付的订单");
Status = OrderStatus.Shipped; RaiseDomainEvent(new OrderShippedEvent(Id, trackingNumber)); }}聚合设计原则
Section titled “聚合设计原则”1. 小聚合原则
Section titled “1. 小聚合原则”聚合应该尽可能小,只包含必须保持一致性的对象:
// ✅ 正确:小聚合public class Order : AggregateRoot<int>{ private readonly List<OrderItem> _items = new(); public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
// Order 和 OrderItem 必须保持一致性}
// ❌ 错误:大聚合public class Order : AggregateRoot<int>{ public Customer Customer { get; set; } // Customer 应该是独立的聚合根 public List<OrderItem> Items { get; set; } public Payment Payment { get; set; } // Payment 可能是独立的聚合根 public Shipment Shipment { get; set; } // Shipment 可能是独立的聚合根}2. 通过 ID 引用其他聚合
Section titled “2. 通过 ID 引用其他聚合”聚合之间不应该直接持有对象引用,而是通过 ID 引用:
// ✅ 正确:通过 ID 引用public class Order : AggregateRoot<int>{ public int CustomerId { get; private set; } // 引用 Customer 聚合的 ID public int ShippingAddressId { get; private set; }}
// ❌ 错误:直接持有对象引用public class Order : AggregateRoot<int>{ public Customer Customer { get; set; } // 直接引用另一个聚合根 public Address ShippingAddress { get; set; }}聚合根设计最佳实践
Section titled “聚合根设计最佳实践”1. 使用工厂方法创建聚合根
Section titled “1. 使用工厂方法创建聚合根”public class Order : AggregateRoot<int>{ private Order() { } // 私有构造函数
public static Order Create(int customerId, Address shippingAddress) { // 验证 if (customerId <= 0) throw new DomainException("客户 ID 无效");
if (shippingAddress == null) throw new DomainException("收货地址不能为空");
var order = new Order { CustomerId = customerId, ShippingAddress = shippingAddress, Status = OrderStatus.Draft, CreatedAt = DateTime.UtcNow };
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id, customerId)); return order; }}2. 使用私有集合和内部构造函数
Section titled “2. 使用私有集合和内部构造函数”public class Order : AggregateRoot<int>{ // 私有集合 private readonly List<OrderItem> _items = new();
// 只读集合对外暴露 public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void AddItem(int productId, string productName, decimal price, int quantity) { // 内部构造函数,只能通过聚合根创建 var item = new OrderItem(productId, productName, price, quantity); _items.Add(item); }}
public class OrderItem : Entity<int>{ // 内部构造函数 internal OrderItem(int productId, string productName, decimal price, int quantity) { ProductId = productId; ProductName = productName; Price = price; Quantity = quantity; }}3. 实现状态转换方法
Section titled “3. 实现状态转换方法”public class Order : AggregateRoot<int>{ public OrderStatus Status { get; private set; }
public void Confirm() { if (Status != OrderStatus.Draft) throw new DomainException($"不能从 {Status} 状态确认订单");
Status = OrderStatus.Confirmed; RaiseDomainEvent(new OrderConfirmedEvent(Id)); }
public void Pay(PaymentInfo paymentInfo) { if (Status != OrderStatus.Confirmed) throw new DomainException($"不能从 {Status} 状态支付订单");
Status = OrderStatus.Paid; RaiseDomainEvent(new OrderPaidEvent(Id, paymentInfo)); }
public void Cancel(string reason) { if (Status == OrderStatus.Shipped || Status == OrderStatus.Completed) throw new DomainException($"不能取消 {Status} 状态的订单");
Status = OrderStatus.Cancelled; RaiseDomainEvent(new OrderCancelledEvent(Id, reason)); }}与仓储的配合
Section titled “与仓储的配合”聚合根是仓储操作的对象:
public class OrderService{ private readonly IOrderRepository _orderRepository;
public async Task<int> CreateOrder(CreateOrderDto dto) { // 创建聚合根 var order = Order.Create(dto.CustomerId, dto.ShippingAddress);
// 添加订单项 foreach (var item in dto.Items) { order.AddItem(item.ProductId, item.ProductName, item.Price, item.Quantity); }
// 持久化聚合根 await _orderRepository.AddAsync(order); await _orderRepository.SaveChangesAsync(); // 领域事件在此时自动分发
return order.Id; }
public async Task ConfirmOrder(int orderId) { // 加载聚合根 var order = await _orderRepository.FindAsync(orderId); if (order == null) throw new NotFoundException("订单不存在");
// 执行领域操作 order.Confirm();
// 保存更改 await _orderRepository.SaveChangesAsync(); }}聚合根 vs 实体
Section titled “聚合根 vs 实体”| 特性 | 聚合根 | 实体 |
|---|---|---|
| 标识 | ✅ 有 | ✅ 有 |
| 可独立存在 | ✅ 是 | ❌ 否 |
| 仓储访问 | ✅ 是 | ❌ 否 |
| 事务边界 | ✅ 是 | ❌ 否 |
| 对外接口 | ✅ 是 | ❌ 否 |
- 聚合应该尽可能小:只包含必须一起修改的对象
- 一个事务只修改一个聚合根:跨聚合操作使用领域事件
- 通过 ID 引用其他聚合:不要直接持有聚合根引用
- 封装聚合内对象:外部只能通过聚合根访问
- 维护聚合不变量:确保聚合始终处于有效状态