Implementing an Extended Domain Model

I recently started reading about a rich domain model instead of anemic models. In all the projects that I worked on before, we followed the service pattern. In my new new project, I am trying to implement a rich domain model. One of the problems I am facing is to decide where the behavior occurs (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 the Item class. Before I add an item to the order, I need to make sure that a valid order ID is passed. Therefore, I perform this check in the AddItem method. Am I on the right track with this? Or do I need to create a validation in the Order class that reports whether the OrderID is valid?

+5
source share
5 answers

Did the order have an AddItem method? The item is added to the order, and not vice versa.

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

In this case, the order is valid because it was created. Of course, the Order does not know that the Element is valid, therefore the validation problem remains. Thus, validation can be added in the AddItem method.

 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 this corresponds to the original concept of OOP about the storage of data and their behavior in the class. However, how is validation performed? Do I need to make a database call? Check stock levels or other things outside the class? If so, pretty soon the Order class is inflated with additional code that is not related to the order, but to check the validity of the element, call external resources, etc. This is not exactly OOPy, and definitely not SOLID.

In the end, it depends. Are classroom behavior needs? How complicated are the behaviors? Can they be used elsewhere? Are they needed only in a limited part of the life cycle of an object? Can they be tested? In some cases, it makes sense to extract behavior into classes that are more focused.

So, build richer classes, make them work and write the appropriate tests. Then look at how they look and smell, and decide if they fit your goals, whether they can be expanded and maintained, or if you need to reorganize them.

+2
source

First of all, each element is responsible for its own state (information). In a good OOP design, an object can never be installed in an invalid state. You should at least try to prevent this.

For this, you cannot have public setters if one or more fields in combination are required.

In your example, Item not valid if orderId or itemId . Without this information, the order cannot be completed.

Therefore, you should implement this class as follows:

 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 guaranteed that the element is in the correct state from the very beginning, forcing and checking the information directly in the constructor.

ItemName is just a bonus, you do not need to process the order.

If property developers are publicly available, it is easy to forget to specify as required fields, thereby getting one or more errors later when this information is processed. Forcing him to turn on, as well as verify the information that you most often break.

Order

The order object must ensure that its entire structure is valid. Thus, he must have a control over the information that he carries, including also the elements of the order.

if you have something like this:

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

You basically say: I have order items, but I don't care how much or what they contain. This is an invitation to errors later 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 reporting something like: Please be nice, check the item first, and then add it via the Add method. However, if you have an order with identifier 1, someone can still make order.AddItem(new Item{OrderId = 2, ItemId=1}) or order.Items.Add(new Item{OrderId = 2, ItemId=1}) , so the order will contain invalid information.

imho a ValidateItem method is not included in Order , but in Item , since it is itself responsible for the correct state.

Best design:

 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 gained control over the entire order, if changes should be made to the list of elements, this should be done directly in the order object.

However, the item can still be changed without specifying the order. Someone can, for example, before order.Items.First(x=>x.Id=3).ApplyDiscount(10.0); , which would be fatal if the order had a cached Total field.

However, good design does not always do this 100% correctly, but it is a compromise between the code we can work with and the code that does everything correctly in accordance with principles and patterns.

+2
source

I would agree with the first part of the dbugger solution, but not with the part where validation is done.

You may ask: β€œWhy not the dbugger code? It is simpler and has fewer methods to implement!” Well, the reason is that the resulting code will be somewhat confusing. Imagine someone using an implementation of dbuggers. He could write code like this:

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

Anyone who does not know the implementation details of the dbugger "IsValid" method simply does not understand what this code should do. Even worse, he or she may also suggest that this will be a comparison between order and subject. This is due to the fact that this method has weak cohesion and violates the principle of sole responsibility of the PLO. Both classes should only be responsible for verifying themselves. If the check also includes checking the reference class (for example, the item in the order), then the item may 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 make sure that you do not call a method that starts an element check from the element check method. The result will be an endless loop.

[Update]

Trailmax has now stated that access to the database from the application domain verification code will be problematic and that it uses the special ItemOrderValidator class to verify.

I totally agree with that. In my opinion, you should never access the database from the application domain model. I know that there are some templates, such as Active Record, that contribute to this behavior, but I believe that the resultig code is always a bit unclean.

So, the main question: how to integrate external dependency in your model with a rich domain.

In my opinion, there are only two valid solutions.

1) Do not. Just make it procedural. Write a service that lives on top of an anemic model. (I think this is Trailmax solution)

or

2) Include (earlier) external information and logic in your domain model. The result is a rich domain model.

Just like Yoda said, "Do it or not." No attempt.

But the initial question was how to create a rich domain model instead of an anemic domain model. Not how to create an anemic domain model instead of a rich domain model.

The resulting classes will 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 asking: you still need a (procedural) service method to load data (order with items) from the database and run the check (loaded order object). But the difference in the anomalous domain model is that this service does NOT contain the validation logic itself. The domain logic is within the domain model, not inside the service / dispatcher / validator or any other name that you call your service classes. Using a rich domain model means that services simply manage various external dependencies, but they do not include domain logic.

So, what if you want to update domain data at a specific point in your domain logic, for example. immediately after calling the IsValidForOrder method?

Well, that will be a problem.

If you really have such a transaction-oriented demand, I would recommend not using a model with a rich domain.

[Update: database identifier identifiers checked - persistence checks should be in the service] [Update: conditional element stock checks added, code clearing]

0
source

To model a compound transaction, use two classes: the transaction class (order) and the LineItem class (OrderLineItem). Each LineItem is then associated with a specific product.

When it comes to behavior, accept the following rule:

"Action on an object in the real world becomes a service (method) of this object in an object-oriented approach."

0
source

If you navigate using the Rich Domain model, use the AddItem method inside Order. But SOLID principles do not want you to check other things inside this method.

Imagine that you have the AddItem () method in the order that checks the item and recounts the total amount of the order, including taxes. The next change is that validation is dependent on the country, language chosen and currency selected. The next tax change varies by country. The following requirements may include verification of transfers, discounts, etc. Your code will become very complex and difficult to maintain. Therefore, I believe that it is better to have such a thing inside AddItem:

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

Now you can check the creation of elements and the addition of goods to the order separately. You can use the IOrderItemBuilder.Build () method for some countries:

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

Thus, you can test and use the code separately for different responsibilities and countries. It’s easy to mock every component and also change them at runtime depending on the user's choice.

0
source

Source: https://habr.com/ru/post/1240742/


All Articles