Let's first consider the case where we have an aggregate which includes a reference to another aggregate. That's perfectly reasonable, provided that the business is satisfied that coordinated changes between the aggregates are eventually consistent.
Now, each of the aggregates has their own commands (each changing their own state). Best practices suggest that we should only be modifying one aggregate per transaction; in other words, we should only be running command(s?) on one aggregate or the other.
Can we organize our code to enforce that?
I've been chewing on a remark from Greg Young, that getters and setters are evil. Setters, sure -- setters should instead be commands, written in the Ubiquitous Language. But getters? how on earth are you going to do anything useful with another object if you can't read it? What are you going to do with a Specification that can't read the object it is supposed to constrain?
I've chosen, for the moment, to understand his comment in this way: getters and setters have no place in the model; getters are perfectly acceptable in an immutable projection.
I'm borrowing these two ideas from Greg;
So if we send a command to a model, and the execution of that command required state from some other aggregate, then we need to hydrate the appropriate projection of the remote aggregate.
I had been blocked on this until recently, because I couldn't see past needing a getter to obtain the reference to the remote aggregate to do the hydration.
But the answer to that puzzle is to pass a DomainService as one of the arguments in the command. The root can look up the referenceId without needing to expose it, and pass that value to the service to get back an immutable projection with precisely the data that it needs.
Essentially, we are building into the signature of the command the contract that promises we won't change anybody else.
Two use cases where I need more thought. The first is factory commands; calls into this aggregate to create a new instance of that aggregate. The second is a query on this aggregate to run a command on that one.
Another perspective on the problem: if the other aggregate is responsible for a business invariant, then it may throw a checked exception. I don't see how I can claim to be implementing a query that changes the model (in another aggregate), or an immutable object that throws exceptions.
My guess right now is that You Don't Do That. Instead, some hand waving happens in the Application Service fronting this mess that gets all the dancers on the correct step.
No comments:
Post a Comment