Bruce
Bruce

Reputation: 1

Domain Driven Design - How To Implement Always Valid State

I have a domain model Order with OrderItems.

An Order must

  1. have a manager
  2. have at least one more orderItem.

My order constructior is like below

public Order(Manager manager, IList<OrderItem> orderItems) 
{
    if(manager == null) throw new ArgumentNullException(nameof(manager));
    if(orderItems == null) throw new ArgumentNullException(nameof(orderItems));
    if(orderItems.Count == 0)
        throw new Exception("List must contain at least one item.");
    foreach(var item in orderItems)
        AddItem(item);
    //assign values
    this.manager = manager;
    ...
    ...
}
Manager manager;
IList<OrderItem> orderItems;
...

void AddItem(OrderItem orderItem)
{
   if(orderItem == null) throw new ArgumentNullException(nameof(orderItem));
   if(orderItems.Contains(orderItem))
       throw new Exception("Order Item duplicate");
   orderItems.Add(orderItem);
}

void CreateNewOrder(int managerId, List<int> itemIdList)
{
    Manager manager = managerRepo.FindById(managerId);
    List<OrderItem> itemList =new List<OrderItem>();
    foreach(int itemId in itemIdList)
        itemList.Add(itemRepo.FindById(itemId));

    Order order = new Order(manager, itemList);
    orderRepo.Add(order);
}

I think its close to a persistence model, not a domain model.

What if i code like below?

public Order(Manager manager)
{ 
    if(manager == null) throw new ArgumentNullException(nameof(manager));
    this.manager = manager;
    ...
    ...
}

public void AddItem(OrderItem orderItem)
{
   if(orderItem == null) throw new ArgumentNullException(nameof(orderItem));
   if(orderItems.Contains(orderItem))
       throw new Exception("Order Item duplicate");
   orderItems.Add(orderItem);
}

public void ReadyForPersistence()
{
    if(orderItems.Count == 0)
        throw new Exception("Not ready for persistence");
}

void CreateNewOrder(int managerId, List<int> itemIdList)
{
    Manager manager = managerRepo.FindById(managerId);
    Order order = new Order(manager);

    //Here order has zero item, does this mean order is in invalid state?

    foreach(int itemId in itemIdList)
        order.AddItem(itemRepo.FindById(itemId));

    order.ReadyForPersistence(); 
    orderRepo.Add(order);
}

Did I misunderstand "Always valid state"?

How could I implement properly "always valid state model".

Upvotes: 0

Views: 313

Answers (2)

M. Lanza
M. Lanza

Reputation: 6790

One way to handle your design dilemma for keeping an entity always valid is to recognize the existence of a state machine. An entity is not restricted to remaining a single type during its entire lifetime. An entity is a uniquely identifiable thing that can sometimes transition between types (metamorphosis).

While it may be true that an order must have a least 1 line item, a shopping cart can have between 0 and n items.

How does one transition states? Well, you make the entity into some kind of addressable thing, like a reference. Once you've made the entity an address, you are free to use immutable objects.

An immutable object is just a persistent data structure that once constructed cannot have it's state mutated. Rather, you execute functions/methods against it that return a modified copy of the original object with some effected change applied. This means that calling any given function may return either the same type (with new data) or a new type (e.g. a state machine transition). With this in mind, you could have an entity transition, somewhere downstream in the workflow, from a ShoppingCart to an Order.

This kind of thing can be done in either languages that support protocols (Clojure, Swift, Groovy, etc.) or languages that support union types (F#, Elm, Haskell, Reason, OCaml). Using either allows the same message (e.g. place) have different behaviors. The place function would on the ShoppingCart validate that it has everything it needs to transition into an Order.

I am not claiming that this is the definitive solution to your problem. I am just putting it out there as a way of handling issues of this variety.

Upvotes: 0

Savvas Kleanthous
Savvas Kleanthous

Reputation: 2745

First of all, I would like to say that persistence is not so relevant to what you're asking. The question really is: is an empty order really something valid in the domain you're trying to model?

If an empty order is something that is wrong or doesn't make any sense in your domain I would say immediately go ahead and enforce this invariant in code. Do not allow any method to complete that would leave the order in an inconsistent state. It is not important that the application level code (in this case the CreateNewOrder method) has a valid order at the end. Another implementor may make a mistake and forget to add items in it; your order would not be enforcing the invariants it needs to in that case.

As a side note, it really depends on the industry you're working on if an empty order makes sense. Do speak with your subject matter experts to check if an empty order isn't just something that has a different name. You may find out that an empty order is something valid, with it's own rules and actions, but they have another name for it and it has a different set of invariants and it could make things vastly simpler for you if it does: you could have an "order draft" as part of your model that will work organically as a factory to your order.

Upvotes: 1

Related Questions