DDD is all about building intention revealing APIs (if we look beyond all the organizational and domain discovery stuff).
Its about encapsulating business concepts and rules inside objects, also known as, good ol’ fashioned object oriented programming.
In this post, I will go through some code smells and patterns that can help you encapsulate your domain logic “better”. We will do this through refactoring these classes;
The demon classThe first class is the Demon class, which is what most of us has probably used and seen many times before. No encapsulation, just properties that gets set by the consuming code.
This code would usually be coupled by something along the lines of
The DemonService class with some factory method.
The DemonService class would have a factory method, for encapsulating the creation and in best case some validation of the input.
Communication and intentions
So what are we communicating with these classes?
From the API, not a whole lot. The intention I am reading is that all of these properties are optional and are allowed to be changed at any time.
For the DemonService class, it would be very similar. The problem we are facing here, is that developers MUST know that the factory method exists in order to get a “valid” demon instance. Having public setters also encourages hacky code to fix bugs “lets just change it quickly and move on”.
Code smell: Public setters equals no way of enforcing consistency
If you look at the Demon class again, you will see that all properties have public getters and setters, this means that we have no way of enforcing consistency for the lifetime of an instance. It could be changed at any point.
Fix: Make the class immutable
This way, at-least we know a Demon is consistent during its lifetime as the properties aren’t changeable.
Code smell: The ‘Valid when we remember’ pattern
Do you remember the DemonService class with the factory method?
Having validation and creation logic separate from the class, might seem nice, separation of concern.. right?.. right? No..
This will again leave to hacky code where “we just need a demon with a name” or similar semantics will get thrown around the codebase. The problem is; That it is not a valid Demon .
IsValid vs Always Valid
IsValid is when we have a method in our class which checks validity (when we call it). Always valid is the idea that an instance should ALWAYS be a valid instance. This way, we know that if we get passed an instance, it will always be ‘correct’.
Fix: Move validation logic to the constructor
Move the validation logic from the factory method into the constructor of the Demon class. We are now Always Valid.
Code smell: Primitive obsession
Looking at the Demon class once again;
Notice how we are using primitives for everything? int’s and string‘s all over the place.
The problem with Primitives is that they probably have way more invariants than what you actually accept and want. Lets take the List<string> Underlings as an example.
How many invariants does a string have? and what is a “valid” underling? especially when we take a List as a parameter. How do we validate the entries?
Fix: Encapsulate it in a class (ValueObject)
Great! Now we can validate an Underling as its own thing, and we are sure to have a “Valid” instance ALWAYS, as we are following the rule of Always Valid.
Code smell: Relationship is communicated through naming
This code smell is closely related to Primitive obsession.
Take a look at PowerDamage and PowerName, they seem to communicate some form of relationship between them, but there isn’t anything enforcing it, sure it could be enforced through validation in the constructor, but what about consumers? How do we communicate this relationship better?
Fix: Encapsulate it in a class (ValueObject)
Now it’s an object of its own and can be used as such, Always Valid and everything…
And in the Demon constructor
Code smell: Exposing the List API
If you notice in the Demon class we might have made the setter private for the Underlings, but since its a List type, we are exposing the List API.
Sometimes, this could be what we want, but not in this case.
Fix: Encapsulate the List and expose an IEnumerable
By encapsulating the List and only exposing an IEnumerable we stop exposing the List API. At the same time we will use the ubiquitous language of our Demon domain for Adding a new Underling. Since we moved the validation logic to the Underling class, we can safely just ‘new’ it up.
Code smell: No rich behavior
If we look long and hard at our Demon class, we will notice that we actually don’t have any rich behavior. We do however have some domain rules surrounding the Demon horns.
In our domain, a Demon can only grow horns, not shed them and it is not “born” with them.
Fix: Encapsulate logic using the ubiquitous language
We simply encapsulate this logic in a method GrowHorns(). This way, consuming code can always add horns to our Demon instance. The domain is whole!
Behavior should only operate on the state it encapsulates. Meaning that behavior inside the Demon class, should only mutate state of the Demon class and classes it encapsulates. — this can be a bit tricky however, and will be up to the designer and domain experts to figure out whether a Demon should change an underling directly or the Underling should encapsulate this logic and the Demon simply calling that logic.
Creation Patterns
If we look at the Demon class, the constructor is what is used for creating a new instance. This is usually fine, but there are some ways to make it even more ubiquitous.
Private constructor + static factory method
This means hiding the constructor from the consumer, and have a static method that calls the constructor. Notice the use of ubiquitos language.
Private constructor + static instances
Looking at the Power class, we would feasibly not want consuming code to simply create new powers. We could force them to use existing powers.
What do we end up communicating?
Looking through the API now, the only way to instantiate a Demon is to call the static factory method Summon().
There are only 2 Powers.
And we communicate effectively what is allowed and the ubiquitous language of our domain.
You can find all of the code here
Tips
Remember to always override equality comparison methods and GetHashCode or simply use the newly added record type.
Put your domain model in its own project. This makes it much easier to reason about what should talk with what, and which projects should depend on which projects.
Repository interfaces and Domain Events should live in the Domain project. NOT Repository implementations. — see 4.
The domain model does NOT care about storage.
Don’t use IAggregateRoot, IEntity and IValueObject interfaces. Some people favor these, but I find them to hurt more than they add value. Marker interfaces are very situational and a side effect of using them in terms of DDD is that you force yourself to see an object as THAT given type, biasing you in that direction. Also these notions in DDD are purely for explaining the sort of hierarchy and dependency between them. There could be reasons to have an interface for the AggregateRoot which would be used by the repositories to make sure ONLY IAggregateRoot objects are sent back from the database. You could also defend an Entity base class (not an interface) like this to force good and sane defaults for equality comparisons and forcing it to have an Id.
The AggregateRoot is ‘just’ the top level Entity. it isn’t NOT an entity so to speak.
Source: Medium - Alexander Wichmann Carlsen
The Tech Platform
Comentarios