Publié par : yoannr | 13 janvier 2013

Reminder about rules & aggregates


Some piece of wisdom from Philip Jander, coming from the following discussion on the ddd/cqrs google group :

 

Let me chime in on this.

This set of problems naturally arises whenever one develops a 
non-trivial domain model. It is obvious, that there is no immediate 
« easy » solution.

Here is my take on this: the problem simply stems from the fact that we 
are considering a collaboration. There are three entities involved: 
Person, Department and Registration. So the possible points of contact are:

a) have Person expose its Age (to be used by Department or Registration)
b) have Person expose an IsOlderThan guard method/function (to be used 
by Department or Registration)
c) move the rule into Person
d) use the readmodel to inject the age via command to the Department or 
Registration
e) have person provide the age

If we are within one context, I personally rule out option d. The reason 
is that coupling via readmodel is even worse than exposing a property. 
It still couples but the coupling is hidden. This is a bit different if 
we are talking about separate contexts, there the age information would 
simple become part of the external interface. But within one context, 
using the readmodel kindof defeats the idea of having a domain model.

So I am left with three options within the domain model. All of them 
couple Person and Department or Registration. Which I consider perfectly 
ok, what would be the point of having a domain model if the objects may 
not interact?
Hence, I will consider different principles in deciding what to choose. 
First, I like to look up Nicola et al’s Streamlined object modeling for 
such questions. The relevant passage is:

« A validation rule verifies a property value against a standard that is 
not dependent on the properties of the other potential collaborators. 
The object with the clearest access to the standard is the owner of the 
collaboration rule. » [SOM p55]
This seems to be either the Department or the Registration, depending on 
formulation of requirements. At any rate this immediately rules out 
option c. Person must not know about rules of the department.

Now SOM make heavy use of getters and setters. They would go for option 
a. Since I instead intend to follow TDA, I cannot use a, which leaves me 
with b or e.
But, actually IsOlderThan is a getter, too. And here I differ from 
Ernst’s implementation. So I play the TDA card again and dismiss option 
b as just a copy of a.

Which leaves me with my desired strategy « have person provide the age ». 
But to whom? Now the registration is the entity mediating the 
collaboration, therefore I suggest that Person checks its own rules, 
department checks its own rules and registration checks the rules 
governing the collaboration. Since they are potentially complex, I 
actually move them into a factory.

In (pseudo)code:

Department{
     RegisterPerson(Person person){
         GuardIfICanRegisterAtAll();

         var registration = new RegistrationFactory();
         registration.SetDepartment(this);

         person.RequestRegistration(registration);

         registration.Do();
     }
}

Person{
     RequestRegistration(RegistrationFactory registration){
         GuardIfICanRegisterAtAll();
         registration.SetPerson(this, this._age);
     }
}

RegistrationFactory{
     SetDepartment(Department department){
         _department = department.Id;
     }
     SetPerson(Person person, Age age){
         _person = person.Id;
         _age = age;
     }

     Do(){
         EnsurePreconditions();
         AgeRule();
         new Registration(RegistrationHandle.Create(), _department, 
_person); // which in turn emits the appropriate event which is 
collected by a UoW.
     }

     EnsurePreconditions(){
         if (_department==null) throw new 
RegistrationFailedDueToInternalError(« Department not provided »);
         if (_person==null) throw new 
RegistrationFailedDueToInternalError(« Person not provided »);
         if (_age==null) throw new 
RegistrationFailedDueToInternalError(« Age not provided »);
     }

     AgeRule(){
         if (_age < Age.FromYears(25)) throw new 
RegistrationFailedPersonTooYoung();   // <– here is the thingy
     }
}

and the command handler

Handle(RegisterPersonInDepartment cmd)
{
        var person = repository.Get<Person>(cmd.PersonId);
        var department = repository.Get<Department>(cmd.DepartmentId);

        department.RegisterPerson(person);
}

(using cqrs/event sourcing with if-it-doesn’t-throw-it-succeeds 
semantics and auto-registration of the dirty aggregate)

This way, encapsulation is preserved, TDA is observed and the rules are 
with their respective owners. Requiring person to provide the age to the 
registration factory is fine – this simply is part of the factory’s 
contract. Person is of course free to deny the collaboration.

 

If we are within one context, I personally rule out option d. The reason is that coupling via readmodel is even worse than exposing a property.

Why?

Glad you asked 🙂

I have five issues with using the readmodel.

#1 is that I believe the business logic of one bounded context using a domain model should be fully encapsulated in that domain model. That includes the interactions and collaborations. Having a path – even it is only for data access – outside of the domain model for the sole purpose of not explicitly accessing that data within the model doesn’t make a lot of sense to me.

#2 is testing. I want to be able to test any domain by itself. And that includes the collaborations which are usually an important part of the model’s behavior. But I do *not* want to have my read model projections be a part of that. 

#3: I once learned that all dependencies in which a domain model participates should point inward to the domain model. This includes both afferent and efferent ones. Having to depend on the readmodel violates that.

#4: Let me ask, what is the reason to invoke the readmodel? Usually I hear « aggregates should not depend on each other ». Frankly, that is nonsense. A domain model consists of classes that of course have interdependencies. While good model design tries to reduce and qualify these dependencies, having *none* is not of value by itself. An aggregate’s fence doesn’t say ‘no access’ but rather ‘take me as a unit or not at all’ to other aggregates.

#5 is that I did it. That part of the system became a black hole pulling more and more of first data and then logic away from the domain model. Over the course of two years it gradually shifted from « nice shortcut » to « nightmare ».

Of course, one can do it, and it will work. But successfully only for some time or for non-complex systems or for systems that do not change. I.o.w. for systems that do not really call for a domain model 🙂

A domain model’s purpose is to encapsulate business logic. The choice between using a read-model or another AR doesn’t change that.

I feel that this statement is incorrect. « Using a read-model » breaks the encapsulation of the domain model as the readmodel is outside of its boundaries. « Another AR » is within these boundaries.

Seems fine. I would have chosen a class design that would avoid having invalid states, to avoid EnsurePreconditions, but beyond that the approach seems reasonable, but perhaps overkill? I’m surprised Yves doesn’t call it « all that » 🙂

Yes, honestly I didn’t put much thought to that factory. There are possibly ways to do that better 🙂

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Catégories

%d blogueurs aiment cette page :