Fabio Marreco
Fabio Marreco

Reputation: 2303

Enforcing invariants with scope on child entity of aggregate root - DDD

I´m trying to understand how to represent certain DDD (Domain Driven Design) rules. Following the Blue Book convention we have:

I´m having a hard time to find the best way to enforce the invariants when clients can have access to internal entities.

This problem of course only happens if the child entity is mutable.

Supose this toy example where you have a Car with four Tire(s). I want to track the usage of each Tire idependently.

Clearly Car is a Aggregate Root and Tire is an Child Entity.

Business Rule: Milage cannot be added to to a single Tire. Milage can only be added to all 4 tires, when attached to a Car

A naive implementation would be:

public class Tire
{
    public double Milage { get; private set;  }
    public DateTime PurchaseDate { get; set; }
    public string ID { get; set; }
    public void AddMilage(double milage) => Milage += milage;
}

public class Car
{
    public Tire FrontLefTire { get; private set; }
    public Tire FrontRightTire { get; private set; }
    public Tire RearLeftTire { get; private set; }
    public Tire RearRightTire { get; private set; }

    public void AddMilage (double milage)
    {
        FrontLefTire.AddMilage(milage);
        FrontRightTire.AddMilage(milage);
        RearLeftTire.AddMilage(milage);
        RearRightTire.AddMilage(milage);
    }

    public void RotateTires()
    {
        var oldFrontLefTire = FrontLefTire;
        var oldFrontRightTire = FrontRightTire;
        var oldRearLeftTire = RearLeftTire;
        var oldRearRightTire = RearRightTire;

        RearRightTire = oldFrontLefTire;
        FrontRightTire = oldRearRightTire;
        RearLeftTire = oldFrontRightTire;
        FrontLefTire = oldRearLeftTire;
    }

    //...
}

But the Tire.AddMilage method is public, meaning any service could do something like this:

Car car = new Car(); //...

// Adds Milage to all tires, respecting invariants - OK
car.AddMilage(200); 

//corrupt access to front tire, change milage of single tire on car
//violating business rules - ERROR
car.FrontLefTire.AddMilage(200); 

Possible solutions that crossed my mind:

  1. Create events on Tire to validate the change, and implement it on Car
  2. Make Car a factory of Tire, passing a TireState on its contructor, and holding a reference to it.

But I feel there should be an easier way to do this.

What do you think ?

Upvotes: 4

Views: 620

Answers (2)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57249

Transient references to internal members can be passed out for use withing a single operation only.

In the years since the blue book was written, this practice has changed; passing out references to internal members that support mutating operations is Not Done.

A way to think of this is to take the Aggregate API (which currently supports both queries and commands), and split that API into two (or more) interfaces; one which supports the command operations, and another that supports the queries.

The command operations still follow the usual pattern, providing a path by which the application can ask the aggregate to change itself.

The query operations return interfaces that include no mutating operations, neither directly, nor by proxy.

root.getA() // returns an A API with no mutation operations
root.getA().getB() // returns a B API with no mutation operations

Queries are queries all the way down.

In most cases, you can avoid querying entities altogether; but instead return values that represent the current state of the entity.

Another reason to avoid sharing child entities is that, for the most part, the choice to model that part of the aggregate as a separate entity is a decision that you might want to change in the domain model. By exposing the entity in the API, you are creating coupling between that implementation choice and consumers of the API.

(One way of thinking of this: the Car aggregate isn't a "car", it's a "document" that describes a "car". The API is supposed to insulate the application from the specific details of the document.)

Upvotes: 2

Robert Bräutigam
Robert Bräutigam

Reputation: 7744

There should be no getters for the Tires.

Getters get you in trouble. Removing the getters is not just a matter of DDD Aggregte Roots, but a matter of OO, Law of Demeter, etc.

Think about why you would need the Tires from a Car and move that functionality into the Car itself.

Upvotes: 0

Related Questions