【问题标题】:Rich Domain Model Implementation富领域模型实现
【发布时间】:2016-01-16 03:13:01
【问题描述】:

我最近开始阅读有关富域模型而不是贫血模型的内容。我之前从事的所有项目,我们都遵循服务模式。在我的新项目中,我正在尝试实现富域模型。我遇到的问题之一是试图确定行为的位置(在哪个类中)。考虑这个例子 -

public class Order
{

   int OrderID;
   string OrderName;

   List<Items> OrderItems;
}

public class Item
{
   int OrderID;
   int ItemID;
   string ItemName;

}

所以在这个例子中,我在 Item 类中有 AddItem 方法。在将商品添加到订单之前,我需要确保传入有效的订单 ID。所以我在 AddItem 方法中进行验证。我在正确的轨道上吗?或者我是否需要在 Order 类中创建验证来判断 OrderID 是否有效?

【问题讨论】:

    标签: c# oop design-patterns


    【解决方案1】:

    Order 没有 AddItem 方法吗?一个项目被添加到订单中,而不是相反。

    public class Order
    {
    
       int OrderID;
       string OrderName;
       List<Items> OrderItems;
       bool AddItem(Item item)
       {
         //add item to the list
       }
    }
    

    在这种情况下,订单是有效的,因为它已被创建。当然,订单不知道商品是否有效,因此存在潜在的验证问题。因此可以在 AddItem 方法中添加验证。

    public class Order
    {
    
       int OrderID;
       string OrderName;
       List<Items> OrderItems;
       public bool AddItem(Item item)
       {
         //if valid
         if(IsValid(item))
         {
             //add item to the list
         }
    
       }
    
      public bool IsValid(Item item)
      {
         //validate
      }
    
    }
    

    所有这些都符合将数据及其行为放在一个类中的原始 OOP 概念。但是,验证是如何执行的?它是否必须进行数据库调用?检查库存水平或班级边界之外的其他东西?如果是这样,Order 类很快就会被与订单无关的额外代码膨胀,但要检查 Item 的有效性,调用外部资源等。这不完全是 OOPy,也绝对不是 SOLID。

    最终,这取决于。行为的需求是否包含在类中?行为有多复杂?它们可以在其他地方使用吗?它们是否仅在对象生命周期的有限部分中需要?他们可以被测试吗?在某些情况下,将行为提取到更集中的类中更有意义。

    所以,构建更丰富的类,让它们工作并编写适当的测试然后看看它们的外观和气味,并确定它们是否符合您的目标,可以扩展和维护,或者是否需要重构。

    【讨论】:

    • 很好的答案,但实体验证自己的富数据模型的核心不是吗?那么为什么你必须验证Order 类中的项目呢?即便如此,为什么Order 的工作是确定Item 是否有效? that 不应该在Item 上吗?
    • 这是问题的症结所在。行为去哪儿了?一个项目可以验证自己吗?需要的验证是否在订单上下文中发生变化?例如,仅当订单中有某些其他商品时,才可以应用折扣商品。折扣本身是有效的,但可能不在某些订单中。
    • 感谢您的回答!!我同意你所说的大部分内容,除了验证一个项目。我认为这应该在项目类中完成。 order 类可以在 item 类中调用该方法,然后才能添加它。顺便说一句,我开始觉得服务模式更适合 Web 应用程序.. 只是一个想法:)
    • 正如我所说,这完全取决于应用程序、它的需求以及它的发展方式。
    • 对象验证自己 - 在限制范围内。订单必须有一个项目(否则它不是订单)。库存水平 - 可能不在订单上检查,而是在处理服务中检查。 UI 可能会要求 - 但从技术上讲,缺货商品的订单仍然有效。
    【解决方案2】:

    首先,每个项目都对自己的状态(信息)负责。在良好的 OOP 设计中,对象永远不会被设置为无效状态。你至少应该尽量阻止它。

    为了做到这一点,如果需要一个或多个字段组合,则不能使用公共设置器。

    在您的示例中,如果Item 缺少orderIditemId,则它是无效的。如果没有这些信息,订单将无法完成。

    因此,您应该像这样实现该类:

    public class Item
    {
       public Item(int orderId, int itemId)
       {
           if (orderId <= 0) throw new ArgumentException("Order is required");
           if (itemId <= 0) throw new ArgumentException("ItemId is required");
    
          OrderId = orderId;
          ItemId = itemId;
       }
    
       public int OrderID { get; private set; }
       public int ItemID { get; private set; }
       public string ItemName { get; set; }
    }
    

    看看我在那里做了什么?我通过直接在构造函数中强制和验证信息来确保项目从一开始就处于有效状态。

    ItemName 只是一个奖励,您不需要它来处理订单。

    如果属性设置器是公开的,那么很容易忘记指定两个必填字段,从而在处理该信息时会出现一个或多个错误。通过强制包含它并验证信息,您可以更早地发现错误。

    订购

    订单对象必须确保它的整个结构是有效的。因此它需要控制它携带的信息,其中也包括订单项目。

    如果你有这样的事情:

    public class Order
    {
       int OrderID;
       string OrderName;
       List<Items> OrderItems;
    }
    

    您基本上是在说:我有订购商品,但我并不真正关心它们包含多少或包含什么。那是在开发过程中的后期招致错误。

    即使你这样说:

    public class Order
    {
       int OrderID;
       string OrderName;
       List<Items> OrderItems;
    
       public void AddItem(item);
       public void ValidateItem(item);
    }
    

    您正在传达类似以下内容:请先验证项目,然后通过 Add 方法添加它。但是,如果您有 id 为 1 的订单,仍然有人可以使用 order.AddItem(new Item{OrderId = 2, ItemId=1})order.Items.Add(new Item{OrderId = 2, ItemId=1}),从而使订单包含无效信息。

    恕我直言,ValidateItem 方法不属于Order,而是属于Item,因为它自己有责任保持处于有效状态。

    更好的设计应该是:

    public class Order
    {
       private List<Item> _items = new List<Item>();
    
       public Order(int orderId)
       {
           if (orderId <= 0) throw new ArgumentException("OrderId must be specified");
           OrderId = orderId;
       }
    
       public int OrderId { get; private set; }
       public string OrderName  { get; set; }
       public IReadOnlyList<Items> OrderItems { get { return _items; } }
    
       public void Add(Item item)
       {
           if (item == null) throw new ArgumentNullException("item");
    
           //make sure that the item is for us
           if (item.OrderId != OrderId) throw new InvalidOperationException("Item belongs to another order");
    
           _items.Add(item);
       }
    }
    

    现在您已经控制了整个订单,如果要对项目列表进行更改,则必须直接在订单对象中完成。

    但是,仍然可以在订单不知情的情况下修改项目。例如,如果订单有缓存的 Total 字段,则有人可以发送到 order.Items.First(x=&gt;x.Id=3).ApplyDiscount(10.0);,这将是致命的。

    但是,好的设计并不总是 100% 正确地做到这一点,而是在我们可以使用的代码和根据原则和模式正确完成所有事情的代码之间进行权衡。

    【讨论】:

    • 很好的答案。我刚刚开始掌握 ddd 并觉得服务模式更适合..考虑这个服务层中 AddItem 方法的示例。无论您传入什么,流程都是这样的 - 检查项目是否有效(基本验证),检查订单 ID 是否有效,您要执行的任何其他自定义验证,最后添加到数据库。这样,任何正在添加的项目都必须经过此验证,并且数据将始终有效,因为它在添加到数据库之前通过了所有这些检查...
    【解决方案3】:

    我同意 dbugger 解决方案的第一部分,但不同意进行验证的部分。

    你可能会问:“为什么不用 dbugger 的代码?它更简单,实现的方法更少!” 原因是生成的代码会有些混乱。 想象一下有人会使用 duggers 实现。 他可能会写出这样的代码:

    [...]
    Order myOrder = ...;
    Item myItem = ...;
    [...]
    bool isValid = myOrder.IsValid(myItem);
    [...]
    

    不知道 dbugger 的“IsValid”方法的实现细节的人根本不会理解这段代码应该做什么。 更糟糕的是,他或她可能还会猜测这将是订单和商品之间的比较。 那是因为这种方法内聚力弱,违反了面向对象的单一职责原则。 两个类都应该只负责验证自己。 如果验证还包括对引用类的验证(如 Order 中的项目),则可以询问该项目是否对特定订单有效:

    public class Item
    {
       public int ItemID { get; set; }
       public string ItemName { get; set; }
    
       public bool IsValidForOrder(Order order) 
       {
       // order-item validation code
       }
    
    }
    

    如果您想使用这种方法,您可能需要注意不要从项目验证方法中调用触发项目验证的方法。结果将是一个无限循环。

    [更新]

    现在 Trailmax 表示从应用程序域的验证代码中访问数据库会出现问题,他使用特殊的 ItemOrderValidator 类来进行验证。

    我完全同意这一点。 在我看来,您永远不应该从应用程序域模型中访问数据库。 我知道有一些像 Active Record 这样的模式会促进这种行为,但我发现结果代码总是有点不干净。

    所以核心问题是:如何在富域模型中集成外部依赖。

    在我看来,只有两个有效的解决方案。

    1) 不要。只是让它程序化。编写一个基于贫血模型的服务。 (我猜这是 Trailmax 的解决方案)

    2) 在你的领域模型中包含(以前的)外部信息和逻辑。结果将是一个富域模型。

    正如尤达所说:做或不做。没有尝试。

    但最初的问题是如何设计丰富的领域模型而不是贫乏的领域模型。 不是如何设计贫乏的领域模型而不是丰富的领域模型。

    生成的类如下所示:

    public class Item
    {
       public int ItemID { get; set; }
       public int StockAmount { get; set; }
       public string ItemName { get; set; }
    
       public void Validate(bool validateStocks) 
       { 
          if (validateStocks && this.StockAmount <= 0) throw new Exception ("Out of stock");
          // additional item validation code
       }
    
    }
    
    public class Order
    {    
      public int OrderID { get; set; }
      public string OrderName { get; set; }
      public List<Items> OrderItems { get; set; }
    
      public void Validate(bool validateStocks)
      {
         if(!this.OrderItems.Any()) throw new Exception("Empty order.");
         this.OrderItems.ForEach(item => item.Validate(validateStocks));        
      }
    
    }
    

    在您问之前:您仍然需要一个(程序)服务方法来从数据库加载数据(带有项目的订单)并触发(加载的订单对象的)验证。 但是与贫血域模型的不同之处在于该服务本身不包含验证逻辑。 域逻辑在域模型中,而不是在服务/管理器/验证器或您调用服务类的任何名称中。 使用富域模型意味着服务只是编排不同的外部依赖项,但它们不包含域逻辑。

    如果您想在域逻辑中的特定点更新域数据怎么办,例如调用“IsValidForOrder”方法后立即?

    好吧,那会是个问题。

    如果你真的有这种面向事务的需求,我建议不要使用富域模型。

    [更新:删除了与 DB 相关的 ID 检查 - 持久性检查应该在服务中] [更新:增加了条件项库存检查,代码清理]

    【讨论】:

    • 问题开始于IsValidForOrder 内部,我们想检查物品的库存水平 - 需要转到数据库或其他 API。在这种情况下,代码会变得混乱:Item 需要依赖数据库服务,这并不好。类可以在一定程度上验证自己。如果验证取决于外部因素,我宁愿有一个特殊的 ItemOrderValidator 类来进行整体验证。
    • 您不应该从域模型访问数据库层。域模型应该只处理域问题,而不是与数据库相关的问题。只需在域中包含信息。或者使用基于服务模式的方法。
    • 我同意你的回答!!!为什么你更喜欢抛出异常而不是返回错误代码?我理解在这个构造函数中,但是添加项方法可能会返回描述错误的错误代码??????
    • 我使用异常是因为如果方法遇到错误(不希望发生的事情),通常会引发异常。如果它不是意外错误而是操作的预期结果(往往会定期发生),您也可以使用返回码。您的选择应取决于您希望它定期发生(结果)还是从不发生(错误)。您还可以编写自己的异常并在其中包含详细信息。
    【解决方案4】:

    要为复合交易建模,请使用两个类:Transaction(Order)和LineItem(OrderLineItem)类。然后每个 LineItem 都与特定的 Product 相关联。

    当涉及到行为时,请遵循以下规则:

    “对现实世界中对象的操作,在面向对象的方法中成为该对象的服务(方法)。”

    【讨论】:

      【解决方案5】:

      如果您使用富域模型,请在 Order 中实现 AddItem 方法。但是 SOLID 原则不希望您在此方法中进行验证和其他事情。

      假设您在 Order 中有 AddItem() 方法来验证商品并重新计算总订单金额(包括税费)。您的下一个更改是验证取决于国家/地区、所选语言和所选货币。您的下一个变化是税收也取决于国家/地区。下一个要求可能是翻译检查、折扣等。您的代码将变得非常复杂且难以维护。所以我觉得在 AddItem 里面有这样的东西会更好:

      public void AddItem(IOrderContext orderItemContext) {
         var orderItem = _orderItemBuilder.BuildItem(_orderContext, orderItemContext);
         _orderItems.Add(orderItem);
      }
      

      现在您可以分别测试项目创建和项目添加到订单。对于某些国家/地区,您的 IOrderItemBuilder.Build() 方法可能是这样的:

      public IOrderItem BuildItem(IOrderContext orderContext, IOrderItemContext orderItemContext) {
          var orderItem = Build(orderItemContext);
          _orderItemVerifier.Verify(orderItem, orderContext);
          totalTax = _orderTaxCalculator.Calculate(orderItem, orderContext);
          ...
          return orderItem;
      }
      

      因此您可以针对不同的职责和国家/地区分别测试和使用代码。很容易模拟每个组件,以及在运行时根据用户选择更改它们。

      【讨论】:

        猜你喜欢
        • 2014-01-05
        • 2020-01-29
        • 1970-01-01
        • 2011-12-03
        • 2011-10-14
        • 2010-12-20
        • 2023-04-05
        • 2014-06-25
        • 2015-12-30
        相关资源
        最近更新 更多