Well written software that is a joy to read and takes a complex problem and presenting it in a well thought out simple structure is rare. I want to draw your attention, not to the great design buzz words but to arguably the lowest part of your system. If you are working in a strongly typed
language, how well are you using your type system? Domain models littered with primitives are a good indication that you are not taking full advantage of your programming language.
If you are using primitives in your domain models, you are not using your type system to its full potential. Replace the primitives with value objects, to create more robust software.
What is a value object?
- It is immutable.
- If all its properties are equal, it is equal.
- It validates itself.
To make this more practical, let us take an example.
The domain we are dealing with works with financial transactions in multiple countries. There are two entities, an account and its associated transactions. The account has information about our agreement with the client like the agreed-upon finance fee, and the transaction has information like the transaction amount.
What can we safely say about this class?
- A valid account might have any combination of the following values
- We could have an account Term of -5000.
- What is that Term? Years, days or seconds? It is not apparent looking at just this code.
- What is the fee, we know that if there is a fee, we can very precisely tell you how many, but
it is unclear what it is? Perhaps it is cows.
- We will not be able to use this class without a bunch of if statements.
There seem to be two types involved in this class, the one is the Term, and that is an indication of how long the account holder has to repay his debt. The current data type does not make sense. How would it be possible to have a negative amount? The other thing is that we could have an
account where the Term has not been set at all. That is even more strange than a negative number. Why would we go through all the effort to create the account and not have a repayment term? We need a better type for this one.
The admin fee and finance fee have the same problem as the Term, how can we have an account without those values been set. Although the values are very precise, they are still missing some very vital information. We are assuming that it is a money amount, and we do not know in what currency that amount is. It might be possible for a developer to mistakenly assign a dollar value to the Finance Fee and another developer thinking that value is in Euro.
If we create a money value type, would that make our code better? Let’s first test out our idea. A brand-new money type would contain a value for the amount and a value for that currency. Then we would be able to say I have 40 Euro. In my book, 40 Euro is 40 Euro. This is how such a money
value object could look.
I am using the value object here based on the work of Vladimir Khorikov and Steven Roberts
In terms of the account Term, we have a data type in C# that will work better than the existing one, and it is a time span. Let us combine these with our money value type and come up with an account that is easier to reason about.
This is better. For starters, we have made sure this object makes sense in the domain. We cannot have an account with a NULL term. We also have been able to make sure that we cannot make accidental mistakes like setting the admin fee to pounds and the finance fee to Euro.
Now that we have done a simple example of the benefits, let’s look at a more involved example.
The next example is doing a simple calculation, using the version of code without the value objects
Let’s first look at the formula
This is how it was implemented:
Some notes on this code
- It is tricky to follow if the code is doing what the formula is saying it should do.
- There is a lot of hardcoded values and extracting them to constants will not make this code much better.
- There is a method that is being used for making sure that the formula can be calculated, this means that there are quite a few places to check if you can use a value.
- There are nine places where there is potential for null reference exceptions; there is an attempt to mitigate these exceptions by checking if the calculation can be done with “IsTransactionFinanceFeeCalculatable”.
- It will be hard to modify this code without reading that method to ensure that the value is not null.
- The question is really “why are there 9 potential nulls?”, how is it possible to have so many nulls in so few lines.
Let us improve this by introducing several value types
There is a couple of things we still need to add to our types to get them where they can be super useful. We need to tell our value types how they can interact with each other. For example, in our formula, we have AdminFee x Tax. Do not do the calculation in the code, do the calculation in the
type, because that is how you get code re-use and the benefit of never ever having to think how money and percent interact in your system.
Adding this operator overload does that for you.
That then allows us to change our transaction to this, that makes it easy and straightforward to focus on what is essential.
Do I still need a developer to read the code and tell the domain expert what it is?
Primitive types have no place in our domain models, strings and integers are terrible to model the complex world of our domain languages. Do not allow external factors like the infrastructure libraries to dictate to you how your models should look. No amount of clean architecture will fix
models that could just as well have been json files.
- Vladimir Khorikov ( https://enterprisecraftsmanship.com/ )
- Martin Fowler https://www.martinfowler.com/bliki/AnemicDomainModel.html
- Domain-Driven Design: Tackling Complexity in the Heart of Software – Eric Evans
- Applying Domain-Driven Design and Patterns – Jimmy Nilsson
- An Ontology-based Approach for Domain-driven Design of Microservice architectures https://dl.gi.de/bitstream/handle/20.500.12116/3944/B24-1.pdf?sequence=1&isAllowed=y