DDD实践疑问:发票上下文的商品编码参考数据需建模为聚合根吗?
嘿,这个问题我太熟了——很多刚接触DDD的朋友在处理跨上下文的参考数据时,都会纠结要不要给这类只读数据套个聚合根的壳,完全能理解你觉得“小题大做”的感受!我来分享下我的实践思路:
结论:绝对不需要把这个商品编码建模成发票上下文的聚合根
为什么?聚合根的核心定位不匹配
聚合根的本质是封装业务规则、管理自身生命周期、作为事务边界的核心领域对象。但你这里的商品编码在发票上下文里完全是个“外来户”:
- 它的生命周期由配置上下文全权管理,发票上下文只同步了Id、Name的只读子集,不需要修改它的任何属性;
- 它在发票流程里的唯一作用就是“合法性校验”——确认这个编码是存在的,没有任何属于发票上下文的业务行为需要依附在它上面;
- 硬把它做成聚合根,反而会凭空增加复杂度:你需要为它实现仓储(但根本不需要写操作)、处理不必要的事务边界,完全违背了DDD“精简领域模型”的原则。
正确的建模方式:值对象+校验服务
针对这个场景,更合理的做法是:
把商品编码建模为值对象(Value Object)
因为它是不可变的,且相等性由Id和Name共同决定,完全符合值对象的特征。比如定义一个ProductCode,包含Id和Name两个属性,没有任何修改方法。用领域服务或应用层做存在性校验
你不需要让聚合根来承担校验责任,而是可以在发票创建的流程中,引入一个专门的校验逻辑:- 如果你的系统里已经同步了配置上下文的商品编码到发票上下文的只读存储(比如缓存或本地表),可以写一个领域服务
ProductCodeValidationService,封装“检查编码是否存在”的逻辑; - 创建发票时,先通过这个服务校验每个商品编码的有效性,没问题再把
ProductCode值对象关联到发票行里。
- 如果你的系统里已经同步了配置上下文的商品编码到发票上下文的只读存储(比如缓存或本地表),可以写一个领域服务
伪代码示例
// 发票上下文的商品编码值对象(只读,不可变) public record ProductCode(Guid Id, string Name); // 领域服务:负责校验商品编码的有效性 public class ProductCodeValidationService { // 这里的仓储是只读的,只负责查询同步过来的商品编码数据 private readonly IReadOnlyProductCodeRepository _readOnlyRepo; public ProductCodeValidationService(IReadOnlyProductCodeRepository readOnlyRepo) { _readOnlyRepo = readOnlyRepo; } // 校验并返回合法的商品编码,不合法则抛出领域异常 public ProductCode GetValidCode(Guid codeId) { var code = _readOnlyRepo.GetById(codeId); if (code == null) { throw new InvalidProductCodeException($"商品编码 {codeId} 不存在"); } return code; } } // 发票聚合根:只关注自己核心的业务规则 public class Invoice : AggregateRoot { public Guid Id { get; private set; } public List<InvoiceLine> Lines { get; private set; } // 私有构造函数,强制通过Create方法创建 private Invoice() => Lines = new List<InvoiceLine>(); public static Invoice Create(Guid invoiceId, List<InvoiceLineRequest> lineRequests, ProductCodeValidationService validator) { var invoice = new Invoice { Id = invoiceId }; foreach (var lineReq in lineRequests) { // 先校验商品编码有效性 var validCode = validator.GetValidCode(lineReq.ProductCodeId); // 创建发票行,关联合法的商品编码值对象 invoice.Lines.Add(InvoiceLine.Create(validCode, lineReq.Quantity, lineReq.UnitPrice)); } // 执行发票自身的业务规则校验(比如总金额不能为负等) invoice.ValidateInvoiceRules(); return invoice; } private void ValidateInvoiceRules() { // 核心业务规则逻辑... } } public class InvoiceLine { public ProductCode ProductCode { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } private InvoiceLine() { } public static InvoiceLine Create(ProductCode productCode, int quantity, decimal unitPrice) { // 发票行的业务规则校验... return new InvoiceLine { ProductCode = productCode, Quantity = quantity, UnitPrice = unitPrice }; } }
额外提醒
如果你的系统不需要在发票上下文存储商品编码的本地副本,也可以直接在应用层调用配置上下文的查询接口做校验——只要保证这个校验是同步的、可靠的就行。核心原则就是:只在领域模型里保留和当前上下文核心业务强相关的元素,参考数据就用最轻量化的方式处理。
内容的提问来源于stack exchange,提问作者Steven Brookes
相关产品推荐
相关产品推荐

