TL;DR
- Command-Query Separation
- Extract Use Cases (Clean/Hexagonal Architecture)
- Domain Events (with Eventhandlers and Composite Pattern)
- Cross-Cutting Concerns (with Decorator Pattern)
Many backend applications end up as a big ball of mud where small changes ripple through the whole codebase. The root causes are high coupling and low cohesion. The best remedies against those are Domain Driven Design (DDD) and the SOLID principles which are well-known, but often wrongly applied.
Even if teams think they practice DDD, they mostly use transaction scripts with an anemic domain model. Services containing these transaction scripts evolve into god classes with thousand lines of code and dozen of dependencies. Such code is hard to test, hard to maintain and hard to comprehend. Does the following code snippet look familiar to you?
|
|
Surrounding all behaviour around one domain object in a single class is an OOP anti-pattern and leads to unsustainable code. In this article, we’ll refactor the code above into a maintainable and decoupled solution with the help of DDD and the SOLID principles. Let’s first find out what SOLID principles are violated and why.
Single-Responsibility Principle (SRP)
A class should only have one reason to change.
The OrderService
has many reasons to change. For example, it must be adopted
when there a changes for the discount calculation logic, the shipping logic, the
order creation logic etc.. Worse yet, due to the numerous dependencies the
OrderService
suffers from low cohesion which also causes the class to be prone
to unrelated changes.
Open-Closed Principle
A class should be open for extension but closed for modification.
The OrderService
’s business logic is impossible to adapt without touching the
existing code. This makes changes error-prone and is a violation of the
Open-Closed Principle. I know it’s hard to imagine how to change behaviour
without touching the code but we’ll see later what is meant by that and how to
resolve the problem.
Interface-Segregation Principle
Clients should not be forced to depend upon interfaces that they do not use.
A consumer of OrderService
is overwhelmed by all the methods provided. If a
consumer only wants to use a single method like createOrder()
, he must depend
on the complete OrderService
interface. Such big interfaces lead to testing
nightmares because in order to create a viable fake, one must implement all
methods even if only one is needed.
Cohesion and Coupling
The SOLID violations above lead to low cohesion and high coupling. As a result,
simple changes will ripple through the whole codebase and are also error-prone
because we have to modify and touch existing code. This leads to high
maintenance effort. In the next paragraphs, we refactor the OrderService
step-by-step and fix the above issues.
Refactorings
Command-Query Separation
Collecting all functionality around one domain class is bad practice. In order
to improve and slim down the big OrderService
, we will split the functionality
into commands and queries. The
Command-Query Separation Principle
from Bertrand Meyer classifies functions into two types:
A command performs side-effects but does not return a value.
A query returns a value but has no side-effects.
The principle helps to write intention-revealing interfaces since readers will be able to detect the type of a function by just looking at its declaration. Thus the code is easier to understand. Don’t confuse it with the similar sounding architecture pattern CQRS.
The restructured OrderService
will be split up into two classes:
The god class is stripped down to half its size but it still violates the
Single-Responsibility and Interface-Segregation Principle. Both classes,
OrderCommandService
and OrderQueryService
, need to be changed because of
multiple reasons and a consumer would still depend on methods which are
potentially not needed.
Extract Use Cases
To fix the SRP and ISP violations, we extract every single method of the
OrderCommandService
and OrderQueryService
into its own Use-Case interface.
Use-Cases are a concept from
Clean-
and Hexagonal Architecture. Maybe
it sounds over-engineered but for an ever-growing enterprise application it will
soon pay off. The new consistent structure makes it obvious for developers where
to add new functionality, keeps the code extensible and testable, and prevents
god classes.
Domain Events
The Use-Case classes became smaller and more structured now, but sometimes even a single Use-Case can grow too big, especially when there are a lot of related side-effects involved:
|
|
A great way to extract such side-effects is to publish DomainEvents which are
processed by EventHandlers. With a generic Interface EventHandler<TEvent>
,
it is possible to register multiple EventHandlers for a single DomainEvent,
whereby each handler has its own responsibility. For example:
- register order in system
- charge customer account
- send a confirmation email
- notify warehouse to prepare order for shipping
By moving the side-effect logic into corresponding EventHandlers, the size of
the CreateOrderUseCase
class will be reduced drastically. Additionally,
testability also improves because EventHandlers can be tested in isolation.
The CreatedOrderUseCase
will depend on the EventHandler<CreatedOrderEvent>
interface. At runtime the CompositeOrderCreatedEventHandler
will be injected
which calls all EventHandlers for the OrderCreatedEvent
. The class diagram:
The Use-Case logic does not look like a Transaction Script any longer and feels like clean OOP:
|
|
|
|
The grouping of all EventHandlers for one specific DomainEvent is done via the Composite Pattern:
|
|
|
|
|
|
|
|
The restructuring enables us to add new functionality without modifying existing
code. Therefore we comply with the Open-Closed principle! If we want to add a
new side-effect, we only have to add a new implementation of
EventHandler<OrderCreatedEvent>
. It is not necessary to touch the original
CreateOrderUseCase
class anymore.
Some intrigued readers may wonder how to test this setup. Spring Boot offers an easy way to replace the EventHandlers with Test Doubles:
|
|
|
|
Cross-Cutting Concerns
We are able to add new functionality without touching existing code but we are restricted to prescribed Domain Events. What if we want to add general functionality like Cross Cutting Concerns:
- Logging
- Transactions
- Metrics (e.g. duration profiling)
The naive approach would be to add the new code into the Use-Case class itself
but we already know that this violates the Open-Closed Principle. Maybe there is
another approach? And yes there is. We can utilize the
Decorator Pattern to enrich
the existing Use-Case with logging, transactional or profiling behaviour. The
following example shows decorators wrapping the CreateOrderUseCase
with
logging and profiling:
|
|
|
|
The next step is to make the decorated UseCase available as a Spring Bean.
Because we have multiple implementations of the UseCase interface, we need a
@Qualifer
annotation for the original and for the fully decorated UseCase.
|
|
|
|
|
|
We added Cross-Cutting concerns to the existing CreateOrderUseCase
. We comply
to the Open-Closed Principle once more! However, the main downside of the
described approach is that we have to write specific decorators for every
Use-Case. This violates the
DRY Principle. One way
to fix this is to find a common interface for all Use-Cases. Then you can write
one decorator and cover all Use-Cases at once. At the beginning we introduced
the Command-Query Separation. It is possible to classify all methods into
commands and queries. For commands, a unifying interface would look like this:
|
|
If your Use-Cases implement the CommandHandler<TCommand>
interface, you can
write one Logging Decorator and it will cover all CommandHandlers aka Use-Cases.
You can find an in-depth elaboration for the
Command Handler Pattern
and even for the
Query Handler Pattern
in Steven van Deursen’s blog.
Final Thoughts
Phew! That was a long refactoring journey but I truly believe, every step was very valuable and showed how to combine SOLID principles and Design Patterns in order to write clean and extensible code. I hope you got inspired and you use the new insights in your own projects.
Finally, I want to mention that this article was heavily inspired by the book Dependency Injection Principles, Practices, and Patterns . Basically it is a rewrite of chapter 10 and ports the C# examples to Java with Spring Boot.
One more thing
Please don’t be dogmatic about the given advice. Use your own judgement when to apply predefined principles and patterns! Don’t be the guy who rewrites the whole codebase with Decorator- and Composite patterns tomorrow. Every situation is different, every scenario requires its own assessment. Never forget:
It depends. - Kent Beck
DDD and SOLID make sense for medium to large projects. A simple CRUD application won’t benefit from over-engineered abstractions. They will be even counter-productive and introduce accidental complexity.