【问题标题】:Injecting domain service into the AggregateRoots in DDD将域服务注入 DDD 中的 AggregateRoots
【发布时间】:2013-06-04 10:40:17
【问题描述】:

DDD 中众所周知的建议是聚合根不使用域服务。域服务是协调两个聚合根来实现一个行为。

当我看到 Rinat Abdullin 写的标题为 Building Blocks Of CQRS 的博客时,我真的很惊讶。在域服务部分下,您将看到域服务被注入到聚合根。

聚合根可以接受域服务吗?

【问题讨论】:

    标签: domain-driven-design domainservices


    【解决方案1】:

    将任何东西注入到域对象中是非常困难的,而且这样做是非常特定于技术的。在java中,它需要在编译时将方面编织到您的域类中。尽管我可能会误会,但我认为大多数 DDD 领导者认为这通常是一个坏主意。 EvansVernon 都积极劝阻,我喜欢听他们的。如需完整说明,请阅读 Vernon。

    【讨论】:

    • 我同意 Evans 和 Vernon 不同意 Rinat 的想法。在示例中,他使用定价服务在 LockCustomerForAccountOverdraft 方法中查找阈值。这是可以通过发送命令来完成的业务规则评估。使用 Evan 和 Verson 的方法,应该有一个称为 LockCustomer 的更高级别的域服务,其中可以完成 PricingService 和 CustomerAggregate 的协调。我认为 Evans 和 Vernon 应用了 SRP,因此有更多组件。 Rinat 采用了一种简单的方法,因此在这种情况下,他可以为客户打破开放/封闭原则。
    【解决方案2】:

    在某种程度上是的。如果 AR 确实需要一个服务来完成它的一些工作,那么您可以将它作为方法参数注入。如果 AR 需要为其大部分行为提供服务,那么它的建模可能不正确。

    【讨论】:

    • 如果两个服务都生活在同一个有界上下文中,聚合根可以通过消息相互通信,那么为什么需要注入服务呢?
    • 只有当 AR 需要它来执行某些行为时才需要它。我看不出消息在这里有什么帮助。
    • 我可能在这里使用了错误的术语。我的意思是域事件。
    • 我理解你,我的答案还是一样
    • @MikeSW +1。因此,如果我需要它来帮助执行我需要 AR 执行的工作(即 ICalculator 或 IPaymentGateway),我会使用接口注入服务。如果服务实际上是在聚合中的东西(比如计算器是我聚合中的某个类),那么我可以“新建”它(除非它使单元测试更难)。更好的是,如果我不需要服务的副作用,那么我可以使用无副作用的域事件(我收集 AR 的域事件,然后应用程序将在事务结束时将它们全部分发,完成 AR 后)。这是正确的吗?
    【解决方案3】:

    我觉得following explanation 非常好。它基于 Vaughn Vernon 的书,并通过实际需要该服务的方法调用将域服务“注入”到域模型中。

    public class PurchaseOrder
    {
        public string Id { get; private set; }
        public string VendorId { get; private set; }
        public string PONumber { get; private set; }
        public string Description { get; private set; }
        public decimal Total { get; private set; }
        public DateTime SubmissionDate { get; private set; }
        public ICollection<Invoice> Invoices { get; private set; }
    
        public decimal InvoiceTotal
        {
            get { return this.Invoices.Select(x => x.Amount).Sum(); }
        }
    
        public bool IsFullyInvoiced
        {
            get { return this.Total <= this.InvoiceTotal; }
        }
    
        bool ContainsInvoice(string vendorInvoiceNumber)
        {
            return this.Invoices.Any(x => x.VendorInvoiceNumber.Equals(
                vendorInvoiceNumber, StringComparison.OrdinalIgnoreCase));
        }
    
        public Invoice Invoice(IInvoiceNumberGenerator generator,
            string vendorInvoiceNumber, DateTime date, decimal amount)
        {
            // These guards maintain business integrity of the PO.
            if (this.IsFullyInvoiced)
                throw new Exception("The PO is fully invoiced.");
            if (ContainsInvoice(vendorInvoiceNumber))
                throw new Exception("Duplicate invoice!");
    
            var invoiceNumber = generator.GenerateInvoiceNumber(
                this.VendorId, vendorInvoiceNumber, date);
    
            var invoice = new Invoice(invoiceNumber, vendorInvoiceNumber, date, amount);
            this.Invoices.Add(invoice);
            DomainEvents.Raise(new PurchaseOrderInvoicedEvent(this.Id, invoice.InvoiceNumber));
            return invoice;
        }
    }
    
    public class PurchaseOrderService
    {
        public PurchaseOrderService(IPurchaseOrderRepository repository,
            IInvoiceNumberGenerator invoiceNumberGenerator)
        {
            this.repository = repository;
            this.invoiceNumberGenerator = invoiceNumberGenerator;
        }
    
        readonly IPurchaseOrderRepository repository;
        readonly IInvoiceNumberGenerator invoiceNumberGenerator;
    
        public void Invoice(string purchaseOrderId,
            string vendorInvoiceNumber, DateTime date, decimal amount)
        {
            // Transaction management, along with committing the unit of work
            // can be moved to ambient infrastructure.
            using (var ts = new TransactionScope())
            {
                var purchaseOrder = this.repository.Get(purchaseOrderId);
                if (purchaseOrder == null)
                    throw new Exception("PO not found!");
                purchaseOrder.Invoice(this.invoiceNumberGenerator,
                    vendorInvoiceNumber, date, amount);
                this.repository.Commit();
                ts.Complete();
            }
        }
    }
    

    【讨论】:

    • 据我了解,PurchaseOrderService 是应用程序服务而不是域服务。
    • @wonderfulworld: IInvoiceNumberGenerator 是域服务
    • IInvoiceNumberGenerator 是域服务,PurchaseOrderService 是应用程序服务。您永远不应该将存储库注入到您的域服务中,因为它们是编排用例而不是业务逻辑所需要的。
    【解决方案4】:

    忽略那篇文章。这是很久以前写的,完全是错误的。如果使用 AggregateRootDomainService 模式实现模块,我建议使用更高的逻辑(例如请求处理程序)来负责:

    1. 加载聚合
    2. 借助域服务执行计算
    3. 相应地改变聚合状态。

    【讨论】:

    • Rinat,您不想在您的这些旧文章上放置一个横幅,以重复您在答案中所写的内容吗?人们经常参考您的博客以获取想法和最佳做法,但您当然知道其中一些描述的做法并不是最好的……没有冒犯,只是一个建议。
    猜你喜欢
    • 1970-01-01
    • 2016-11-15
    • 1970-01-01
    • 2015-01-03
    • 1970-01-01
    • 1970-01-01
    • 2017-03-13
    • 2019-08-23
    • 2016-06-02
    相关资源
    最近更新 更多