Reputation: 2303
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 aCar
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:
events
on Tire
to validate the change, and implement it on Car
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
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
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