1

ALWAYS Valid Domain Model

 2 years ago
source link: https://codeopinion.com/always-valid-domain-model/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Always having your Domain Model in a valid state means it will be predictable. You’ll write less defensive code or conditional code because your domain objects will always be in a valid state. Using aggregates is a great way to encapsulate the state with behavior to keep the state valid. Using factories to create your aggregates is key to having a valid state from the very beginning. Here’s how you can create an always valid domain model.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Aggregate

First, let’s start with defining our Aggregate as it ultimately is what will keep a valid domain model.

The example I’m going to use in this post is of a Shipment. You could think of a Shipment in the sense of Food Delivery service where you’re ordering food from the restaurant and it gets delivered to your home.

The aggregate in this scenario consists of a Shipment and two Stops. There is a Pickup Stop, which is the restaurant where the shipment starts from, and a Delivery stop which is your home where the Shipment ends. The Aggregate Root is the Shipment.

One important aspect of this is that each stop needs to follow a progression. The initial state of a Stop is “In Transit”, then goes to “Arrived” once the delivery driver arrives at the location of the stop (either the restaurant or your home) and then finally goes to the “Departed” state when the delivery driver leaves the stop.

Another rule is that this progression must be done for the Pickup Stop first, in its entirety before the Delivery Stop can start its progression. This makes sense because you need to arrive at the restaurant to pick up the food, leave the restaurant, arrive at the house for delivery, then leave the house.

Invariants

Based on the simplistic example above, our invariants are:

  • Shipment must have at least 2 stops
  • The First stop must be a Pickup
  • The Last Stop must be a Delivery
  • Stops must progress in order

The first three invariants must be established upon trying to create the Aggregate. The final invariant is controlled within the aggregate. In order to always be in a valid state, we must only allow the creation of a valid Aggregate that satisfies the invariants defined above. And once we have our Aggregate, we must only allow valid state transitions.

Enforcing these invariants is what will keep a valid domain model.

Factory

In order to create our Aggregate in a valid state right from the get-go, we can use a Factory. In the example below, I have a private constructor but expose two different static factory methods that force us into a good state. These factories are enforcing the first three invariants.

public class ShipmentAggregateRoot { private SortedList<int, Stop> Stops { get; } = new();

private ShipmentAggregateRoot(IReadOnlyList<Stop> stops) { for(var x = 0; x < stops.Count; x++) { Stops.Add(x, stops[x]); } }

public static ShipmentAggregateRoot Factory(PickupStop pickup, DeliveryStop delivery) { return new ShipmentAggregateRoot(new Stop[] { pickup, delivery }); }

public static ShipmentAggregateRoot Factory(Stop[] stops) { if (stops.Length < 2) { throw new InvalidOperationException("Shipment requires at least 2 stops."); }

if (stops.First() is not PickupStop) { throw new InvalidOperationException("First stop must be a Pickup"); }

if (stops.Last() is not DeliveryStop) { throw new InvalidOperationException("Last stop must be a Delivery"); }

return new ShipmentAggregateRoot(stops); }

public void Arrive(int stopId) { var currentStop = Stops.SingleOrDefault(x => x.Value.StopId == stopId); if (currentStop.Value == null) { throw new InvalidOperationException("Stop does not exist."); }

var previousStopsAreNotDeparted = Stops.Any(x => x.Key < currentStop.Key && x.Value.Status != StopStatus.Departed); if (previousStopsAreNotDeparted) { throw new InvalidOperationException("Previous stops have not departed."); }

currentStop.Value.Arrive(); }

public void Pickup(int stopId) { var currentStop = Stops.SingleOrDefault(x => x.Value.StopId == stopId); if (currentStop.Value == null) { throw new InvalidOperationException("Stop does not exist."); }

if (currentStop.Value is not PickupStop) { throw new InvalidOperationException("Stop is not a pickup."); }

currentStop.Value.Depart(); }

public void Deliver(int stopId) { var currentStop = Stops.SingleOrDefault(x => x.Value.StopId == stopId); if (currentStop.Value == null) { throw new InvalidOperationException("Stop does not exist."); }

if (currentStop.Value is not DeliveryStop) { throw new InvalidOperationException("Stop is not a delivery."); }

currentStop.Value.Depart(); }

public bool IsComplete() { return Stops.All(x => x.Value.Status == StopStatus.Departed); } }

The Arrive() method is enforcing the last invariant that we must progress our stops in the correct order. Finally, here are the Stops (Pickup, Delivery) that enforce they transition themselves in the correct order.

public class PickupStop : Stop { public PickupStop(int stopId, Address address, DateTime scheduled) : base(stopId, address, scheduled) { StopId = stopId; Address = address; } }

public class DeliveryStop : Stop { public DeliveryStop(int stopId, Address address, DateTime scheduled) : base(stopId, address, scheduled) { StopId = stopId; Address = address; } }

public abstract class Stop { public int StopId { get; protected set; } public StopStatus Status { get; private set; } = StopStatus.InTransit; public Address Address { get; protected set;} public DateTime Scheduled { get; } public DateTime? Departed { get; protected set; }

public Stop(int stopId, Address address, DateTime scheduled) { StopId = stopId; Address = address; Scheduled = scheduled; }

public void Arrive() { if (Status != StopStatus.InTransit) { throw new InvalidOperationException("Stop has already arrived."); }

Status = StopStatus.Arrived; }

public void Depart() { if (Status == StopStatus.Departed) { throw new InvalidOperationException("Stop has already departed."); }

if (Status == StopStatus.InTransit) { throw new InvalidOperationException("Stop hasn't arrived yet."); }

Status = StopStatus.Departed; Departed = DateTime.UtcNow; } }

public enum StopStatus { InTransit, Arrived, Departed }

Draft Mode

Often a scenario is what I call a “Draft Mode” where the invariants aren’t applicable, yet. In other words, they want to create a model that has much looser constraints.

To illustrate this with my Shipment example, you may have multiple Orders to a single Restaurant that ultimately will be placed all on the same Shipment.

3-300x209.png

In this case, the Shipment still has all the invariants but what we likely want is to have our Shipment created from a Plan. The concept of a Plan is to associate multiple orders and then generate a Shipment from them. This means we’re creating an Aggregate from another Aggregate.

ALWAYS Valid Domain Model

Having your domain model always in a valid state, right from the beginning means you’ll have to write less defensive code because you absolutely know the data is in a valid state. In my example, there will always be at least 2 stops, the first will be a Pickup, the last will be a Delivery. All stops will go through a progression, in order.

The factory is what sets everything up in a valid state and the Aggregate keeps us in a valid state.

Source Code

Developer-level members of my CodeOpinion YouTube channel get access to the full source for any working demo application that I post on my blog or YouTube. Check out the membership for more info.

Related Posts

Follow @CodeOpinion on Twitter

Leave this field empty if you're human:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK