【问题标题】:Getting Additional Data for a Domain Entity获取域实体的附加数据
【发布时间】:2009-08-07 02:10:57
【问题描述】:

我有一个域聚合,称之为“订单”,其中包含一个订单行列表。订单跟踪订单行上的金额总和。客户有一个正在运行的“信用”余额,他们可以从中订购,这是通过汇总他们的数据库交易历史来计算的。一旦他们用完“池”中的所有钱,他们就不能再订购任何产品了。

因此,每次将一行添加到订单中时,我都需要检查池中还剩下多少,以及订单是否将它们推过它。池中的金额不断变化,因为其他相关客户不断使用它。

问题是,从 DDD 的角度考虑,我如何获得这个数量,因为我不想用 DataContext 问题污染我的域层(在此处使用 L2S)。由于我不能只从域中查询到数据库,我将如何获取该数据以便验证业务规则?

这是使用领域事件的实例吗?

【问题讨论】:

    标签: domain-driven-design aggregates


    【解决方案1】:

    您的订单聚合应完全封装。因此,它需要能够确定添加项目是否有效,即是否超出客户信用。有多种方法可以做到这一点,但它们都依赖于 Order 存储库返回一个特定的聚合,该聚合知道如何执行此特定操作。例如,这可能与您用于满足订单的订单聚合不同。

    在这种情况下,您必须识别并在代码中捕获您期望订单履行特定角色的事实,即添加额外订单项的角色。为此,您可以为此角色创建一个接口,并为该角色创建一个内部支持的相应聚合。

    然后,您的服务层可以向您的订单存储库请求满足此显式角色接口的订单,因此存储库有足够的信息来说明您需要什么来构建可以满足该要求的东西。

    例如:

    public interface IOrder
    {
      IList<LineItem> LineItems { get; }
      // ... other core order "stuff"
    }
    
    public interface IAddItemsToOrder: IOrder
    {
      void AddItem( LineItem item );
    }
    
    public interface IOrderRepository
    {
      T Get<T>( int orderId ) where T: IOrder;
    }
    

    现在,您的服务代码如下所示:

    public class CartService
    {
      public void AddItemToOrder( int orderId, LineItem item )
      {
        var order = orderRepository.Get<IAddItemsToOrder>( orderId );
        order.AddItem( item );
      }
    }
    

    接下来,实现IAddItemsToOrder 的 Order 类需要一个客户实体,以便它可以检查信用余额。因此,您只需通过定义特定接口来级联相同的技术。订单存储库可以调用客户存储库以返回履行该角色的客户实体并将其添加到订单聚合中。

    因此,您将拥有一个基本的ICustomer 接口,然后以ICustomerCreditBalance 接口的形式从它派生出一个显式角色。 ICustomerCreditBalance 既充当客户存储库的标记接口,告诉它您需要客户做什么,所以它可以创建适当的客户实体,并且它具有支持特定角色的方法和/或属性。比如:

    public interface ICustomer
    {
      string Name { get; }
      // core customer stuff
    }
    
    public interface ICustomerCreditBalance: ICustomer
    {
      public decimal CreditBalance { get; }
    }
    
    public interface ICustomerRepository
    {
      T Get<T>( int customerId ) where T: ICustomer;
    }
    

    显式角色接口为存储库提供他们需要的关键信息,以正确决定从数据库中获取哪些数据,以及是急切地还是延迟地获取数据。

    请注意,在这种情况下,我已将CreditBalance 属性放在ICustomerCreditBalance 接口上。但是,它也可以在基础ICustomer 接口上,ICustomerCreditBalance 然后变成一个空的“标记”接口,让存储库知道您将要查询信用余额。这一切都是为了让存储库知道您希望它返回的实体的角色。

    正如您在问题中提到的,将这一切结合在一起的最后一部分是域事件。如果超出客户的信用余额,订单可以引发故障域事件,以通知服务层订单无效。另一方面,如果客户有足够的信用,它可以更新客户对象上的余额或引发域事件以通知系统的其余部分需要减少余额。

    我没有将域事件代码添加到 CartService 类,因为这个答案已经很长了!如果您想了解有关如何执行此操作的更多信息,我建议您针对该特定问题发布另一个问题,我将在那里进行扩展;-)

    【讨论】:

    • 迈克,就聚合设计而言,这是一个很好的答案。如果我在下一个 sprint 中有机会,我会尝试重构到你这里的内容。我解决这个问题的方法是使用类似于 Udi Dahan 最近的文章 [udidahan.com/2009/06/14/domain-events-salvation/].当表示添加行的事件被触发时,事件处理程序检查余额并在聚合上设置一个内部属性。现在,业务规则检查该属性并将成功或失败报告给客户端。
    • 我认为域事件应该用于一种“即发即弃”模式的通知。拥有它们的动机是允许通知/回调,而不会将任何一方耦合到另一方。您的域事件解决方案是完全可行的,但它将它们用作一种分离的远程过程调用。我认为最好使用消息传递隐喻为单向通知保留域事件。这极大地有助于可扩展性 - 例如,您可以使用服务总线来实现它们。
    • 我忘了说:Udi 之前关于域事件的两篇文章 (udidahan.com/2008/02/29/…)回答了你的问题 - 他使用域事件来表示验证失败和 我在这里提倡进行外部验证检查的技术。所以你在 Udi 的领域事件文章中以 1 的价格获得 2 ;-)
    • @Mike Scott:那么您是否在 Repository.Get 中键入检查 T 以确定适当的功能?如果是这样……那,再加上空的标记界面,让我有点不安。我真的很喜欢这个概念。但是,我可能会以不同的方式实现它。给了我一些思考。谢谢!
    • 是什么让你对空接口感到不安?您不必进行显式类型检查。相反,我为给定的行为创建了一个通用接口,例如对于获取策略,我有 IFetchingStrategy 然后我有一个实现它的类,例如CustomerCreditBalanceFetchingStrategy:IFetchingStrategy 提供控制急切或延迟加载等所需的信息。存储库可以通过各种方式获取此信息,例如从 IoC 容器或服务定位器请求 IFetchingStrategy
    【解决方案2】:

    在这种情况下,我使用事件或委托来减轻责任。也许最简单的方法就是使用一些代码来展示。

    您的订单类将有一个Predicate&lt;T&gt;,用于确定客户的信用额度是否足够大以处理订单。

    public class Order
    {
        public Predicate<decimal> CanAddOrderLine;
    
        // more Order class stuff here...
    
        public void AddOrderLine(OrderLine orderLine)
        {
            if (CanAddOrderLine(orderLine.Amount))
            {
                OrderLines.Add(orderLine);
                Console.WriteLine("Added {0}", orderLine.Amount);
            }
            else
            {
                Console.WriteLine(
                    "Cannot add order.  Customer credit line too small.");
            }
        }
    }
    

    您可能会有一个 CustomerService 类或类似的东西来提取可用的信用额度。在添加任何订单行之前设置 CanAddOrderLine 谓词。这将在每次添加行时检查客户的信用。

    // App code.
    var customerService = new CustomerService();
    var customer = new Customer();
    var order = new Order();
    order.CanAddOrderLine = 
        amount => customerService.GetAvailableCredit(customer) >= amount;
    
    order.AddOrderLine(new OrderLine { Amount = 5m });
    customerService.DecrementCredit(5m);
    

    毫无疑问,您的实际情况会比这更复杂。您可能还想查看Func&lt;T&gt; 代表。委托或事件对于在下订单后减少信用金额或在客户超出订单中的信用额度时触发某些功能可能很有用。

    祝你好运!

    【讨论】:

    • 我喜欢这样。我现在正在做的是将服务注入到域对象中,所以它不知道它从哪里获得数量。在概念上与您在这里所做的类似。我会试试这个,看看我有多喜欢它。谢谢凯文!
    • 如果它适合你,那没关系。另一种方法是将域模型保持在比域服务更低的层中。这感觉像是更好的关注点分离并有助于测试。我真的很喜欢使用 Jeffrey Palermo 的 Onion Architecture。你应该检查一下。 jeffreypalermo.com/blog/the-onion-architecture-part-1
    • 域实体应该能够执行这些业务需求,而无需特殊注入接口、谓词等。关键是让您的存储库返回为所需的确切角色量身定制的对象在那一刻。这完全使您的服务和应用程序代码不必了解关于域逻辑的任何内容,这些逻辑完全封装在您的域实体中。因此,您的代码最终的意图更加清晰,没有一堆杂乱的包袱。
    • @Kevin,领域事件(感谢 Udi Dahan)让我摆脱了注入服务的情况。工作精美。 DDD/洋葱架构风格的关注点分离让我对注入服务感到不舒服。
    • @jlembke:乌迪是个天才。看起来很合身!您应该发布您的解决方案并回答您自己的问题。
    【解决方案3】:

    除了获取“池”值的问题(我将使用 OrderRepository 上的方法查询该值)之外,您是否考虑过锁定影响这个问题?

    如果“池”不断变化,是否有可能在您的规则通过之后,但在您将更改提交到数据库之前,其他人的事务会悄悄进入?

    Eric Evans 在他的书(“聚合”)的第 6 章中提到了这个问题。

    【讨论】:

    • 维杰,你是 100% 正确的。这是我们失眠的下一个问题。我错过了这本书的那一部分。我现在就读!!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多