You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

聚合根引用建模疑问: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:13:10