user3587180
user3587180

Reputation: 1397

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. 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. Am I on the right track with this? Or do I need create validation in Order class that tells if the OrderID is valid?

Upvotes: 7

Views: 4561

Answers (5)

Dzianis Yafimau
Dzianis Yafimau

Reputation: 2016

If you go with Rich Domain Model implement AddItem method inside Order. But SOLID principles don't want you validation and other things inside this method.

Imagine you have AddItem() method in Order that validates item and recalculate total order sum including taxes. 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:

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:

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.

Upvotes: 1

aryeh
aryeh

Reputation: 979

To model a composite transaction, use two classes: a Transaction (Order) and a LineItem (OrderLineItem) class. Each LineItem is then associated with a particular Product.

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."

Upvotes: 0

Frank
Frank

Reputation: 2547

I would agree with the first part of dbugger's solution, but not with the part where the validation takes place.

You might ask: "Why not dbugger's code? It's simpler and has less methods to implement!" Well the reason is that the resulting code would be somewhat confusing. Just imagine someone would use dbuggers implementation. 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. 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. 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.

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.

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. Just make it procedural. Write a service that lives on top of an anemic model. (I guess that is Trailmax's solution)

or

2) Include the (formerly) external information and logic in your domain model. 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, e.g. immediately after the "IsValidForOrder" method is called?

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]

Upvotes: 1

jgauffin
jgauffin

Reputation: 101150

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. 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. 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.

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. 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.

imho a ValidateItem method doesn't belong in Order but in Item as it is its own responsibility to be in a valid state.

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); which would be fatal if the order had a cached Total field.

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.

Upvotes: 3

dbugger
dbugger

Reputation: 16399

Wouldn't the Order have the AddItem method? 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.

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. 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.

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.

Upvotes: 2

Related Questions