丰富的域模型实现

时间:2016-01-16 03:13:02

标签: c# oop design-patterns

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

public class Order
{

   int OrderID;
   string OrderName;

   List<Items> OrderItems;
}

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

}

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

5 个答案:

答案 0 :(得分:2)

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。

最终,这取决于。是行为&#39;课堂上包含的需求?这些行为有多复杂?他们可以在别处使用吗?它们是否仅在物体生命周期的有限部分中需要?他们可以测试吗?在某些情况下,将行为提取到更集中的类更有意义。

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

答案 1 :(得分: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);
}

您正在传达的信息如下:请保持良好状态,先验证项目,然后通过添加方法添加项目。但是,如果您的订单ID为1,则某人仍然可以执行order.AddItem(new Item{OrderId = 2, ItemId=1})order.Items.Add(new Item{OrderId = 2, ItemId=1}),从而使订单包含无效信息。

imho 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);
   }
}

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

但是,如果订单不知道,仍然可以修改项目。例如,如果订单有一个缓存的order.Items.First(x=>x.Id=3).ApplyDiscount(10.0);字段,那么有人可以Total这将是致命的。

然而,优秀的设计并不总是100%正确地完成它,而是在我们可以使用的代码和根据原则和模式完成所有事情的代码之间进行权衡。

答案 2 :(得分:0)

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

您可能会问:&#34;为什么不使用dbugger的代码?它更简单,实施方法更少!&#34; 那么原因是结果代码会有些混乱。 想象一下有人会使用dbuggers实现。 他可能会写这样的代码:

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

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

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这样的模式会促进这种行为,但我发现resultig代码总是有点不洁净。

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

从我的观点来看,只有两个有效的解决方案。

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));        
  }

}

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

那么,如果您想在域逻辑中的特定位置更新域数据,例如,紧跟在&#34; IsValidForOrder&#34;方法叫做?

嗯,那就是问题。

如果您真的有这样一个面向事务的需求,我建议不要使用丰富的域模型。

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

答案 3 :(得分:0)

要为复合事务建模,请使用两个类:事务(Order)和 LineItem (OrderLineItem)类。然后,每个LineItem与特定的产品相关联。

在行为方面采用以下规则:

  

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

答案 4 :(得分:0)

如果你使用Rich Domain Model在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;
}

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