简体   繁体   English

丰富的域模型实现

[英]Rich Domain Model Implementation

I recently started reading about rich domain model instead of anemic models. 我最近开始阅读富域模型而不是贫血模型。 All the projects I worked on before, we followed service pattern. 我之前工作的所有项目都遵循服务模式。 In my new new project I'm trying to implement rich domain model. 在我的新项目中,我正在尝试实现丰富的域模型。 One of the issues I'm running into is trying to decide where the behavior goes in (in which class). 我遇到的一个问题是试图确定行为的位置(在哪个类中)。 Consider this example - 考虑这个例子 -

public class Order
{

   int OrderID;
   string OrderName;

   List<Items> OrderItems;
}

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

}

So in this example, I have the AddItem method in Item class. 所以在这个例子中,我在Item类中有AddItem方法。 Before I add an Item to an order, I need to make sure a valid order id is passed in. So I do that validation in AddItem method. 在我向订单添加Item之前,我需要确保传入有效的订单ID。所以我在AddItem方法中进行验证。 Am I on the right track with this? 我是否在正确的轨道上? Or do I need create validation in Order class that tells if the OrderID is valid? 或者我是否需要在Order类中创建验证,以告知OrderID是否有效?

Wouldn't the Order have the AddItem method? Order不会有AddItem方法吗? An Item is added to the Order, not the other way around. 物品被添加到订单中,而不是相反。

public class Order
{

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

In which case, the Order is valid, because it has been created. 在这种情况下,订单有效,因为它已创建。 Of course, the Order doesn't know the Item is valid, so there persists a potential validation issue. 当然,订单不知道该项有效,因此存在潜在的验证问题。 So validation could be added in the AddItem method. 因此可以在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
  }

}

All of this is in line with the original OOP concept of keeping the data and its behaviors together in a class. 所有这些都符合原始的OOP概念,即将数据及其行为保持在一个类中。 However, how is the validation performed? 但是,验证是如何进行的? Does it have to make a database call? 它是否必须进行数据库调用? Check for inventory levels or other things outside the boundary of the class? 检查库存水平或班级边界以外的其他事项? If so, pretty soon the Order class is bloated with extra code not related to the order, but to check the validity of the Item, call external resources, etc. This is not exactly OOPy, and definitely not SOLID. 如果是这样,Order类很快就会增加与订单无关的额外代码,但要检查Item的有效性,调用外部资源等。这不完全是OOPy,绝对不是SOLID。

In the end, it depends. 最后,这取决于。 Are the behaviors' needs contained within the class? 这些行为的需求是否包含在课程中? How complex are the behaviors? 这些行为有多复杂? Can they be used elsewhere? 他们可以在别处使用吗? Are they only needed in a limited part of the object's life-cycle? 它们是否仅在对象生命周期的有限部分中需要? Can they be tested? 他们可以测试吗? In some cases it makes more sense to extract the behaviors into classes that are more focused. 在某些情况下,将行为提取到更集中的类更有意义。

So, build out the richer classes, make them work and write the appropriate tests Then see how they look and smell and decide if they meet your objectives, can be extended and maintained, or if they need to be refactored. 因此,构建更丰富的类,使它们工作并编写适当的测试然后看看它们的外观和气味,并决定它们是否符合您的目标,是否可以扩展和维护,或者是否需要重构。

First of all, every item is responsible of it's own state (information). 首先,每个项目都由它自己的状态(信息)负责。 In good OOP design the object can never be set in an invalid state. 在良好的OOP设计中,永远不能将对象设置为无效状态。 You should at least try to prevent it. 你应该至少试着阻止它。

In order to do that you cannot have public setters if one or more fields are required in combination. 为此,如果需要组合使用一个或多个字段,则无法使用公共设置器。

In your example an Item is invalid if its missing the orderId or the itemId . 在您的示例中,如果Item缺少orderIditemId该项无效。 Without that information the order cannot be completed. 没有该信息,订单无法完成。

Thus you should implement that class like this: 因此,您应该像这样实现该类:

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

See what I did there? 看看我在那里做了什么? I ensured that the item is in a valid state from the beginning by forcing and validating the information directly in the constructor. 我通过在构造函数中直接强制和验证信息,确保项目从一开始就处于有效状态。

The ItemName is just a bonus, it's not required for you to be able to process an order. ItemName只是一个奖励,您不需要处理订单。

If the property setters are public, it's easy to forget to specify both the required fields, thus getting one or more bugs later when that information is processed. 如果属性设置器是公共的,则很容易忘记指定所需的两个字段,从而在处理该信息时获得一个或多个错误。 By forcing it to be included and also validating the information you catch bugs much earlier. 通过强制包含它并验证信息,您可以更早地捕获错误。

Order 订购

The order object must ensure that it's entire structure is valid. 订单对象必须确保其整个结构有效。 Thus it need to have control over the information that it carries, which also include the order items. 因此,它需要控制它所携带的信息,其中还包括订单项。

if you have something like this: 如果你有这样的事情:

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

You are basically saying: I have order items, but I do not really care how many or what they contain . 你基本上是在说: 我有订单商品,但我并不关心它们含有多少或含有什么 That is an invite to bugs later on in the development process. 这是对开发过程中的错误的邀请。

Even if you say something like this: 即使你说这样的话:

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

   public void AddItem(item);
   public void ValidateItem(item);
}

You are communicating something like: Please be nice, validate the item first and then add it through the Add method . 您正在传达类似的信息: 请保持良好状态,首先验证项目,然后通过Add方法添加它 However, if you have order with id 1 someone could still do order.AddItem(new Item{OrderId = 2, ItemId=1}) or order.Items.Add(new Item{OrderId = 2, ItemId=1}) , thus making the order contain invalid information. 但是,如果您的订单ID为1,则仍可以执行order.AddItem(new Item{OrderId = 2, ItemId=1})order.Items.Add(new Item{OrderId = 2, ItemId=1}) ,因此使订单包含无效信息。

imho a ValidateItem method doesn't belong in Order but in Item as it is its own responsibility to be in a valid state. imho一个ValidateItem方法不属于Order但属于Item因为它自己有责任处于有效状态。

A better design would be: 更好的设计是:

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

Now you have gotten control over the entire order, if changes should be made to the item list, it has to be done directly in the order object. 现在您可以控制整个订单,如果要对项目列表进行更改,则必须直接在订单对象中完成。

However, an item can still be modified without the order knowing it. 但是,如果订单不知道,仍可以修改项目。 Someone could for instance to order.Items.First(x=>x.Id=3).ApplyDiscount(10.0); 例如,有人可以order.Items.First(x=>x.Id=3).ApplyDiscount(10.0); which would be fatal if the order had a cached Total field. 如果订单有一个缓存的Total字段,这将是致命的。

However, good design is not always doing it 100% properly, but a tradeoff between code that we can work with and code that does everything right according to principles and patterns. 然而,良好的设计并不总是100%正确地完成它,而是在我们可以使用的代码和根据原则和模式完成所有事情的代码之间进行权衡。

I would agree with the first part of dbugger's solution, but not with the part where the validation takes place. 我同意dbugger解决方案的第一部分,但不同意验证发生的部分。

You might ask: "Why not dbugger's code? It's simpler and has less methods to implement!" 您可能会问:“为什么不使用dbugger的代码?它更简单,实现的方法更少!” Well the reason is that the resulting code would be somewhat confusing. 那么原因是结果代码会有些混乱。 Just imagine someone would use dbuggers implementation. 想象一下有人会使用dbuggers实现。 He could possibly write code like this: 他可能会写这样的代码:

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

Someone who doesn't know the implementation details of dbugger's "IsValid" method would simply not understand what this code is supposed to do. 不知道dbugger的“IsValid”方法的实现细节的人根本不会理解这段代码应该做什么。 Worse that that, he or she might also guess that this would be a comparison between an order and an item. 更糟糕的是,他或她也可能猜测这将是订单和商品之间的比较。 That is because this method has weak cohesion and violates the single responsibility principle of OOP. 这是因为这种方法的内聚力弱,违反了OOP的单一责任原则。 Both classes should only be responsible for validating themself. 这两个班级只应负责验证自己。 If the validation also includes the validation of a referenced class (like item in Order), then the item could be asked if it is valid for a specific order: 如果验证还包括对引用类的验证(如订单中的项目),则可以询问该项是否对特定订单有效:

public class Item
{
   public int ItemID { get; set; }
   public string ItemName { get; set; }

   public bool IsValidForOrder(Order order) 
   {
   // order-item validation code
   }

}

If you want to use this approach, you might want to take care that you don't call a method that triggers an item validation from within the item validation method. 如果要使用此方法,可能需要注意不要在项验证方法中调用触发项验证的方法。 The result would be an infinite loop. 结果将是无限循环。

[Update] [更新]

Now Trailmax stated that acessing a DB from within the validation-code of the application domain would be problematic and that he uses a special ItemOrderValidator class to do the validation. 现在,Trailmax声称从应用程序域的验证代码中访问数据库会有问题,并且他使用特殊的ItemOrderValidator类来进行验证。

I totally agree with that. 我完全同意。 In my opinion you should never access the DB from within the application domain model. 在我看来,您永远不应该从应用程序域模型中访问数据库。 I know there are some patterns like Active Record, that promote such behaviour, but I find the resultig code always a tiny bit unclean. 我知道有一些像Active Record这样的模式会促进这种行为,但我发现resultig代码总是有点不洁净。

So the core question is: how to integrate an external dependency in your rich domain model. 所以核心问题是:如何在富域模型中集成外部依赖。

From my point of view there are just two valid solutions to this. 从我的角度来看,只有两个有效的解决方案。

1) Don't. 1)不要。 Just make it procedural. 只是使它成为程序性的。 Write a service that lives on top of an anemic model. 写一个生活在贫血模型之上的服务。 (I guess that is Trailmax's solution) (我猜这是Trailmax的解决方案)

or 要么

2) Include the (formerly) external information and logic in your domain model. 2)在您的域模型中包含(以前的)外部信息和逻辑。 The result will be a rich domain model. 结果将是一个丰富的域模型。

Just like Yoda said: Do or do not. 就像尤达说:做或不做。 There is no try. 没有尝试。

But the initial question was how to design a rich domain model instead of an anemic domain model. 但最初的问题是如何设计富域模型而不是贫血域模型。 Not how to design an anemic domain model instead of a rich domain model. 不是如何设计贫血域模型而不是富域模型。

The resulting classes would look like this: 结果类看起来像这样:

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

}

Before you ask: you will still need a (procedural) service method to load the data (order with items) from the DB and trigger the validation (of the loaded order-object). 在你提出问题之前:你仍然需要一个(程序)服务方法来从数据库加载数据(带有项目的顺序)并触发(加载的订单对象的)验证。 But the difference to an anemic domain model is that this service does NOT contain the validation logic itself. 但贫血领域模型的不同之处在于该服务本身不包含验证逻辑。 The domain logic is within the domain model, not within the service/manager/validator or whatever name you call your service classes. 域逻辑位于域模型中,不在服务/管理器/验证器中,也不在您调用服务类的任何名称中。 Using a rich domain model means that the services just orchestrate different external dependencies, but they don't include domain logic. 使用富域模型意味着服务只是编排不同的外部依赖项,但它们不包含域逻辑。

So what if you want to update your domain-data at a specific point within your domain logic, eg immediately after the "IsValidForOrder" method is called? 那么,如果您想在域逻辑中的特定点更新域数据,例如在调用“IsValidForOrder”方法后立即更新,该怎么办?

Well, that would be problem. 好吧,那就是问题。

If you really have such a transaction-oriented demand I would recommend not to use a rich domain model. 如果您真的有这样一个面向事务的需求,我建议不要使用丰富的域模型。

[Update: DB-related ID checks removed - persistence checks should be in a service] [Update: Added conditional item stock checks, code cleanup] [更新:删除了与DB相关的ID检查 - 持久性检查应该在服务中] [更新:添加条件项库存检查,代码清理]

To model a composite transaction, use two classes: a Transaction (Order) and a LineItem (OrderLineItem) class. 要为复合事务建模,请使用两个类: Transaction (Order)和LineItem (OrderLineItem)类。 Each LineItem is then associated with a particular Product . 然后,每个LineItem与特定产品相关联。

When it comes to behavior adopt the following rule: 在行为方面采用以下规则:

"An action on an object in the real world, becomes a service (method) of that object in an Object Oriented approach." “对现实世界中的对象采取行动,成为面向对象方法中该对象的服务(方法)。”

If you go with Rich Domain Model implement AddItem method inside Order. 如果你使用Rich Domain Model在Order中实现AddItem方法。 But SOLID principles don't want you validation and other things inside this method. 但是SOLID原则不希望您在此方法中进行验证和其他事情。

Imagine you have AddItem() method in Order that validates item and recalculate total order sum including taxes. 想象一下,你在Order中有AddItem()方法来验证项目并重新计算总订单金额,包括税金。 You next change is that validation depends on country, selected language and selected currency. 您接下来的更改是验证取决于国家/地区,所选语言和所选货币。 Your next change is taxes depends on country too. 你的下一个变化是税收也取决于国家。 Next requirements can be translation check, discounts etc. Your code will become very complex and difficult to maintenance. 下一个要求可以是翻译检查,折扣等。您的代码将变得非常复杂且难以维护。 So I thing it is better to have such thing inside AddItem: 所以我觉得在AddItem中有这样的东西更好:

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

Now you can test item creation and item adding to the order separately. 现在,您可以单独测试项目创建和项目添加到订单。 You IOrderItemBuilder.Build() method can be like this for some country: 对于某些国家/地区,IOrderItemBuilder.Build()方法可以是这样的:

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

So you can test and use separately code for different responsibility and country. 因此,您可以针对不同的责任和国家单独测试和使用代码。 It is easy to mock each component, as well as change them at runtime depending on user choice. 很容易模拟每个组件,并根据用户的选择在运行时更改它们。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM