聚合根引用建模疑问:Customer与AddressBook互依赖如何设计?
解决Customer与AddressBook相互依赖的DDD聚合设计问题
这确实是DDD建模中很常见的困惑点——当两个领域对象彼此绑定、缺一不可,同时又都有自己的不变量需要保护时,核心要抓住聚合的一致性边界这个核心原则来设计。
核心思路:将两者纳入同一个聚合,以Customer为聚合根
既然Customer和AddressBook缺一不可,它们就应该属于同一个一致性边界(即同一个聚合),而不是两个独立的聚合根。这样既能保证两者的生命周期绑定,又能通过聚合根统一管控所有不变量。
具体设计步骤如下:
1. 确定聚合根角色
选择Customer作为聚合根——因为从业务语义上,Customer是更核心的业务实体,AddressBook是依附于Customer的附属实体(即便它有自己的不变量)。聚合根的职责是管理整个聚合的生命周期,外部只能通过Customer访问AddressBook,避免直接操作AddressBook破坏一致性。
2. 用工厂方法确保聚合完整创建
为了禁止单独创建Customer或AddressBook,我们需要:
- 把Customer和AddressBook的构造函数设为私有/内部访问权限(比如C#的
private或internal,Java的private或包级私有),外部无法直接实例化。 - 创建一个专门的
CustomerFactory,提供唯一的创建入口,该方法要求传入创建Customer和AddressBook所需的所有参数,在内部同时完成两者的实例化和不变量验证。
示例代码(C#):
public class CustomerFactory { public Customer CreateCustomer(Guid customerId, string fullName, List<Address> initialAddresses) { // 先验证AddressBook的不变量(比如不能有重复类型的地址) var addressBook = new AddressBook(initialAddresses); // 再验证Customer的不变量(比如姓名不能为空) var customer = new Customer(customerId, fullName, addressBook); return customer; } }
3. 在聚合内部维护不变量
Customer作为聚合根,要负责协调自身和AddressBook的交互,确保所有不变量(包括跨实体的不变量)都被遵守:
- AddressBook的修改操作(比如添加地址)要设为内部方法,只能由Customer调用。
- Customer可以暴露AddressBook的只读视图,防止外部直接修改。
示例代码:
public class Customer { private readonly AddressBook _addressBook; // 私有构造函数,仅工厂可调用 private Customer(Guid id, string fullName, AddressBook addressBook) { if (string.IsNullOrWhiteSpace(fullName)) throw new ArgumentException("Customer full name cannot be empty", nameof(fullName)); Id = id; FullName = fullName; _addressBook = addressBook ?? throw new ArgumentNullException(nameof(addressBook)); } public Guid Id { get; } public string FullName { get; private set; } // 暴露只读的地址列表,外部无法直接修改 public IReadOnlyList<Address> Addresses => _addressBook.Addresses.AsReadOnly(); // 外部通过Customer添加地址,由Customer管控跨实体不变量 public void AddAddress(Address newAddress) { // 比如:限制客户最多拥有5个地址 if (_addressBook.Addresses.Count >= 5) throw new InvalidOperationException("Customer cannot have more than 5 addresses"); // 调用AddressBook的内部方法完成添加,同时AddressBook会验证自身不变量 _addressBook.AddAddress(newAddress); } } public class AddressBook { private readonly List<Address> _addresses = new(); // 内部构造函数,仅聚合内可调用 internal AddressBook(List<Address> initialAddresses) { // 验证自身不变量:不能有重复类型的地址 var duplicateTypes = initialAddresses .GroupBy(a => a.Type) .Where(g => g.Count() > 1) .Select(g => g.Key); if (duplicateTypes.Any()) throw new InvalidOperationException($"Duplicate address types: {string.Join(", ", duplicateTypes)}"); _addresses.AddRange(initialAddresses); } public IReadOnlyList<Address> Addresses => _addresses.AsReadOnly(); // 内部方法,仅Customer可调用 internal void AddAddress(Address address) { if (_addresses.Any(a => a.Type == address.Type)) throw new InvalidOperationException($"Address type {address.Type} already exists"); _addresses.Add(address); } }
为什么这是合理的?
- 避免独立创建:通过私有/内部构造函数,外部无法单独实例化Customer或AddressBook,必须通过工厂创建完整的聚合。
- 统一不变量保护:聚合根Customer负责管控所有跨实体的不变量,AddressBook负责自身的不变量,两者配合确保整个聚合的一致性。
- 生命周期绑定:AddressBook的生命周期完全依附于Customer,当Customer被删除时,AddressBook也会被一起删除,避免数据孤岛。
如果未来业务需求变化,AddressBook需要独立存在(比如多个Customer共享一个AddressBook),那时再考虑将其拆分为独立聚合根即可——但就当前“缺一不可”的需求而言,上述设计是最贴合DDD原则的方案。
内容的提问来源于stack exchange,提问作者Piotr Podraza
相关产品推荐
相关产品推荐

