Chapter 6. SOLID Principles

We all know the principles introduced by Robert C. Martin in the early 2000s. They are milestones in the evolution of OOP and they are still valid ten years later.

However, when applied to a domain model, they need some small adjustments based upon the unfortunate experiences of a few experienced developers facing to their first domain model.

[Caution]

We do not pretend to talk out each principle, as Martin’s books are a must read for developers using Epic.

We heartily recommend the principles described, as they will increase the stability of your models, improving their quality even from a design point of view.

Single Responsibility

 

A class should have only one reason to change.

 
 -- Robert C. Martin Agile Software Development, Principles, Patterns, and Practices (2002)

In the context of the SRP, Martin defines a responsibility to be "a reason for change". He states that if you can think of more than one motive for changing a class, than that class has more than one responsibility. However, whenever no change actually occur, separating responsibility could smell of needless complexity.

In a domain model rightly partitioned in contexts, there are only tree legitimate reasons for change:

  • business evolution,
  • deeper insight,
  • bug fixes.

Since all profitable enterprises evolve, the models that capture their processes will need to be updated. This kind of changes can not be anticipated and they affect almost everything in an application built with Epic.
The good news is that the customer acknowledges about the change he is asking for and he is willing to pay. [5]

Whenever you get a deeper insight into a domain, you are in a economical dilemma: should we change the code or not? And why? Just to clarify the model’s expression through the code?
Finding the right answer to these questions is hard and should be done carefully. You should consider factors like the complexity of the new model and of the previous one, the time available, the long term cost of postponing the refactoring (a cost always undervalued, that slowly increases each day), the cost of all workarounds that you’ll have to implement without the new model and so on.
My own, personal, opinion is that most of times the refactoring should be planned with the customer. If it’s not a trivial task, you should never hide to the customer the problems emerged with the previous model.

Finally, as there’s no software without bug, all fixes will force you to modify the model. This could seem quite obvious, but it has a nasty side effect: you could introduce new subtle untracked bugs. Unit tests' code coverage could reduce the risks but can not eliminate them. [6] Fortunatly, since no bug should relate to contracts (or it will lead you to a far more expensive refactoring toward a deeper insight), all the code depending on the broken component don’t even need to be recompiled. Indeed, one of the reason that led us to introduce a thin layer of pure interfaces around the domain model implementation was the evaluation of the deploy’s costs of bug fixes. With such interfaces, we can minimize the number of layers affected by a bug in the domain, reducing the system stops and the loss for our customer.

Open-Closed

 

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

 
 -- Robert C. Martin Agile Software Development, Principles, Patterns, and Practices (2002)

The golden unachievable ideal of OCP would allow to change a system behaviour by adding new code, not by changing old code that already works.
This would lead to an easy versioning, as each new version would be backward compatible.

Well defined contexts are the first tool for approaching that ideal. Indeed when you define small contexts, you reduce the probability of changes in any single one. Moreover, if the business rules are the only force that drives your design, the code will be as stable as that rules.

For example, if your software handles vineyards and your customer buys a new wheat field, you can sell him a new context instead of adding new classes to the previous one.

To further keep the model closed to modification you could take cheap design decisions that do not smell of needless complexity. For example, since our models run in different technological environments (ASP.NET pages, data loaders, web services and WinForm applications) we choosed to mark all the classes belonging to the domain model with the SerializableAttribute. Moreover, when we got the first customer that required realtime synchronization between different applications running the same domain, we designed the Single Mutable State pattern (that we adopted massively from then on).

[Tip]

Feel free to violate the OCP of modules only when the business evolves and you can’t simply add a new context.

The interface layer that surrounds the model allows a limited flexibility to the code but whenever either the business changes or you get a deeper insight of the context you will have to break the OCP, with a new version of the code.

Template methods are useful when the modelers already know that a domain entity will have some specializations that do not violate the LSP. However you should never leave a class unsealed just to keep an open door behind.

The good news is that technological changes will not affect the model itself: you can replace Epic without any model’s recompilation.

Liskov Substitution

 

Subtypes must be substitutable for their base types.

 
 -- Robert C. Martin Agile Software Development, Principles, Patterns, and Practices (2002)

We’ve already said that no modeler should introduce in the model’s abstractions ignored by the domain experts, since they lead to smell of rigidity and long term bugs.

We value the LSP a lot, since we had some very expensive experiences with its violations. While the principle heavily reduces the applicability of object-oriented inheritance, it avoids the infamous yo-yo problem: bug fixes are cheaper, if you can rapidly understand the model’s code.

We learnt that apply this principle to the domain-driven design, we need that no entity is abstracted beyond its own identifier: all the entities in a hierarchy must share the same kind of identifiers. For example, all type of securities can be identified by their ISIN (or their RIC, or so).
Though we see an evident abstraction between two types, we avoid it’s introduction until the domain expert give us the green light. Moreover he has to explain us how the abstraction is useful in the context.

Value objects abstractions follow the same rule, but do not have any constraint related to identifiers, of course. For example, we never needed to introduce a quantity abstraction.

Interface Segregation

 

Clients should not be forced to depend on methods that they do not use.

 
 -- Robert C. Martin Agile Software Development, Principles, Patterns, and Practices (2002)

The simple definition that Martin provides for ISP give us a big advise: the definition of the context’s boundary is a fundamental step in the domain-driven development process. Indeed, small contexts contain well defined contracts that lead to small classes.

Shared kernels are another marvelous tool to enforce interface segregation: for example they contain base contracts shared between different contexts.

In the idiomatic DDDSample provided with the Epic source code, you can see how the IVoyage violate the ISP: it has query methods used from a ILongshoreman when loading cargos into each voyage, and commands for user impersonating an IPortAuthority.
To enforce the interface segregation, we could write a shared kernel containing at least VoyageNumber, UnLocode and an +IVoyage like this:

With such a kernel, the ICargo interface would have taken this interface instead of the bigger Challenge00.DDDSample.Voyage.IVoyage.

However in this case, we had two good reason to violate the ISP:

  • IVoyage and ICargo are in the same assembly and share the same fate;
  • The hypothetical customer (A.C.M.E.) do not seem interessed into a software to manage ships, planes or freight train (that would lead to a different IVoyage in a new context);
  • We need two roles with different permissions over the same class to show the Epic.Security system.

However in our financial domains we have a substantial shared kernel full of such kind of interfaces and shared identifiers.

Dependency Inversion

 
  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.
 
 -- Robert C. Martin Agile Software Development, Principles, Patterns, and Practices (2002)

The dependency inversion is probably the most valuable feature that Epic provides in a domain-driven application. Indeed it is designed to allow a new, extreme, unexplored dependency inversion between the application and the domain model.
Instead of a domain model that depends on a framework, we build a framework designed to depend on the domain itself, adapting the architecture to the business of the customer.

In Epic, the highest level module is the domain model. It does not depend on anything else that interfaces belonging to the model itself. (a)

Low-level modules as user interface, databases, logging, web services, statistical reports, service bus and so on, always depend on the domain model’s interfaces. (b)

Epic itself is a low-level module, when compared with the domain. It works like an adapter and a manager serving the other modules, but you could replace it with the next wonderful tecnology without affecting any existing models.

In the introduction to the second part of this manual, the DIP will be further analyzed in the context of the Epic’s architecture.



[5] How will be explained into the third part of the book, in such a case you should consider whether to write a new bounded context or modify the current one. The number of production environments running the current code could be a persuasive argument in one sense or another.

[6] I think that the domain model implementation should take pride in a full code coverage (100%). This seems expensive at first, when you are at 97% and you can’t find a way to test those dirty three lines. However, a full code coverage will force you to read your code over and over again, drastically improving the quality of the implementation itself. The ROI of such kind of revision is often bigger than that of the tests themselves as safety net.