--- title: 聚合根 description: 聚合的根实体,是仓储操作和事务边界的入口点 --- 聚合根是聚合的根实体,是仓储操作和事务边界的入口点。它负责维护聚合内所有对象的一致性。 ## 什么是聚合根? 在 DDD 中,聚合(Aggregate)是一组相关对象的集合,这些对象作为一个整体来维护业务规则的一致性。聚合根是聚合的根实体,外部对聚合的所有访问都必须通过聚合根进行。 **核心特点:** - 聚合的唯一对外接口 - 事务边界 - 负责维护聚合内的不变量 - 仓储只能操作聚合根 ## 定义聚合根 在 MiCake 中,聚合根需要继承 `AggregateRoot` 基类: ### 使用整数 ID ```csharp using MiCake.DDD.Domain; using System.Collections.Generic; using System.Linq; public class Order : AggregateRoot { private readonly List _items = new(); public string CustomerName { get; private set; } public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; } // 只暴露只读集合 public IReadOnlyCollection 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 { 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 ```csharp public class Product : AggregateRoot // 等同于 AggregateRoot { public string Name { get; private set; } public decimal Price { get; private set; } public int Stock { get; private set; } } ``` ### 使用其他类型的 ID ```csharp // 使用 GUID public class Customer : AggregateRoot { public string Name { get; private set; } public string Email { get; private set; } } // 使用字符串 public class Tenant : AggregateRoot { public string Name { get; private set; } public bool IsActive { get; private set; } } ``` ## 聚合根特性 ### 1. 聚合内对象的封装 聚合根应该封装聚合内的所有对象,外部不能直接修改它们: ```csharp public class Order : AggregateRoot { private readonly List _items = new(); // ✅ 正确:返回只读集合 public IReadOnlyCollection 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 { public List Items { get; set; } // 外部可以直接修改 } ``` ### 2. 维护不变量 聚合根负责维护聚合内的业务规则(不变量): ```csharp public class ShoppingCart : AggregateRoot { private readonly List _items = new(); private const int MaxItemCount = 100; private const decimal MaxTotalAmount = 50000m; public IReadOnlyCollection 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. 领域事件 聚合根可以触发领域事件来通知领域中发生的重要变化: ```csharp public class Order : AggregateRoot { 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)); } } ``` ## 聚合设计原则 ### 1. 小聚合原则 聚合应该尽可能小,只包含必须保持一致性的对象: ```csharp // ✅ 正确:小聚合 public class Order : AggregateRoot { private readonly List _items = new(); public IReadOnlyCollection Items => _items.AsReadOnly(); // Order 和 OrderItem 必须保持一致性 } // ❌ 错误:大聚合 public class Order : AggregateRoot { public Customer Customer { get; set; } // Customer 应该是独立的聚合根 public List Items { get; set; } public Payment Payment { get; set; } // Payment 可能是独立的聚合根 public Shipment Shipment { get; set; } // Shipment 可能是独立的聚合根 } ``` ### 2. 通过 ID 引用其他聚合 聚合之间不应该直接持有对象引用,而是通过 ID 引用: ```csharp // ✅ 正确:通过 ID 引用 public class Order : AggregateRoot { public int CustomerId { get; private set; } // 引用 Customer 聚合的 ID public int ShippingAddressId { get; private set; } } // ❌ 错误:直接持有对象引用 public class Order : AggregateRoot { public Customer Customer { get; set; } // 直接引用另一个聚合根 public Address ShippingAddress { get; set; } } ``` ## 聚合根设计最佳实践 ### 1. 使用工厂方法创建聚合根 ```csharp public class Order : AggregateRoot { 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. 使用私有集合和内部构造函数 ```csharp public class Order : AggregateRoot { // 私有集合 private readonly List _items = new(); // 只读集合对外暴露 public IReadOnlyCollection 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 { // 内部构造函数 internal OrderItem(int productId, string productName, decimal price, int quantity) { ProductId = productId; ProductName = productName; Price = price; Quantity = quantity; } } ``` ### 3. 实现状态转换方法 ```csharp public class Order : AggregateRoot { 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)); } } ``` ## 与仓储的配合 聚合根是仓储操作的对象: ```csharp public class OrderService { private readonly IOrderRepository _orderRepository; public async Task 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); } // 持久化聚合根(由环境 UoW 统一提交;领域事件在提交时自动分发) await _orderRepository.AddAsync(order); await _unitOfWork.CommitAsync(); return order.Id; } public async Task ConfirmOrder(int orderId) { // 加载聚合根 var order = await _orderRepository.FindAsync(orderId); if (order == null) throw new NotFoundException("订单不存在"); // 执行领域操作 order.Confirm(); // 提交更改(由 UoW 统一提交) await _unitOfWork.CommitAsync(); } } ``` ## 聚合根 vs 实体 | 特性 | 聚合根 | 实体 | |------|--------|------| | 标识 | ✅ 有 | ✅ 有 | | 可独立存在 | ✅ 是 | ❌ 否 | | 仓储访问 | ✅ 是 | ❌ 否 | | 事务边界 | ✅ 是 | ❌ 否 | | 对外接口 | ✅ 是 | ❌ 否 | ## 注意事项 1. **聚合应该尽可能小**:只包含必须一起修改的对象 2. **一个事务只修改一个聚合根**:跨聚合操作使用领域事件 3. **通过 ID 引用其他聚合**:不要直接持有聚合根引用 4. **封装聚合内对象**:外部只能通过聚合根访问 5. **维护聚合不变量**:确保聚合始终处于有效状态 ## 下一步 - 了解如何使用[仓储](/domain-driven/repository/)持久化聚合根 - 学习[领域事件](/domain-driven/domain-event/)处理跨聚合操作 - 探索[工作单元](/domain-driven/unit-of-work/)管理事务