All projects start with a lot of enthusiasm. As many projects grow the technical debt gets bigger and the enthusiasm gets less. Almost any developer can develop a great project, but the key is maintaining an ever evolving application with minimal technical debt without loosing enthusiasm.
During this talk you will be taken on the journey of application design. The starting point is an application that looks fine but contains lots of potential pitfalls. We will address the problems and solve them with beautiful design. We end up with testable, nicely separated software with a clear intention.
18. public function __construct(string $name, string $email)
{
$this->setName($name);
$this->setEmail($email);
}
private function setName(string $name)
{
if ('' === $name) {
throw new InvalidArgumentException('Name is required');
}
$this->name = $name;
}
private function setEmail(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
$this->email = $email;
}
Model
20. private function setEmail(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('E-mail is invalid');
}
$this->email = $email;
}
Model
25. final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
}
}
Value object
26. final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('E-mail is invalid');
}
}
}
Value object
27. final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('E-mail is invalid');
}
$this->email = $email;
}
}
Value object
28. final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('E-mail is invalid');
}
$this->email = $email;
}
public function toString() : string;
}
Value object
29. final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('E-mail is invalid');
}
$this->email = $email;
}
public function toString() : string;
public function equals(EmailAddress $email) : bool;
}
Value object
30. class User
{
public function __construct(EmailAddress $email, string $name)
{
$this->setEmail($email);
$this->setName($name);
}
//[..]
}
Model
47. Some observation
We are now directly using doctrine for persistence
A change of persistence would mean changing
every class where we save or retrieve the user
48. public function registerAction(Request $request) : Response
{
// [..]
$user = new User(new EmailAddress($data['email']), $data['name']);
$this->em->persist($user);
$this->em>flush();
}
Controller
70. class RegisterUser // A command always has a clear intention
{
public function __construct(string $email, string $name)
{
$this->email = $email;
$this->name = $name;
}
// Getters
}
Command
73. class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
$user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName());
$this->userRepository->save($user);
}
}
Command handler
74. class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
$user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName());
$this->userRepository->save($user);
$this->userRegisteredNotifier->notify($registerUser->getEmail()), $registerUser->getName());
}
}
Command handler
75. public function registerAction(Request $request) : Response
{
if ($form->isValid()) {
// [..]
$data = $form->getData();
$this->registerUserHandler->handle(
new RegisterUser($data['email'], $data['name'])
);
}
}
Controller
76. public function registerAction(Request $request) : JsonResponse
{
// [..]
$this->registerUserHandler->handle(
new RegisterUser($data['email'], $data['name'])
);
return new JsonResponse([]);
}
Controller
85. A command bus is a generic command handler
Commands and the
command bus
86. Commands and the
command bus
A command bus is a generic command handler
It receives a command and routes it to the handler
87. Commands and the
command bus
A command bus is a generic command handler
It receives a command and routes it to the handler
It provides the ability to add middleware
88. A great command bus
implementation
github.com/SimpleBus/MessageBus
89.
90. public function handle($command, callable $next)
{
$this->logger->log($this->level, 'Start, [command => $command]);
$next($command);
$this->logger->log($this->level, 'Finished', [command' => $command]);
}
Logging middleware example
91. public function handle($command, callable $next)
{
if ($this->canBeDelayed($command)) {
$this->commandQueue->add($command);
} else {
$next($command);
}
}
Queueing middleware example
100. class UserIsRegistered // An event tells us what has happened
{
public function __construct(int $userId, string $emailAddress, string $name)
{}
// Getters
}
Event
102. class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
//[..]
}
Model
103. class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self
{
}
//[..]
}
Model
104. class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self
{
$user = new self($email, $name);
}
//[..]
}
Model
105. class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self
{
$user = new self($email, $name);
$user->record(new UserIsRegistered($user->id, (string) $email, $name));
}
//[..]
}
Model
106. class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self
{
$user = new self($email, $name);
$user->record(new UserIsRegistered($user->id, (string) $email, $name));
return $user;
}
//[..]
}
Model
107. class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
// save user
foreach ($user->recordedMessages() as $event) {
$this->eventBus->handle($event);
}
}
}
Command handler
116. We now have a rich user model
It contains data
117. We now have a rich user model
It contains data
It contains validation
118. We now have a rich user model
It contains data
It contains validation
It contains behaviour
119. βObjects hide their data behind abstractions and expose
functions that operate on that data. Data structure expose
their data and have no meaningful functions.β
Robert C. Martin (uncle Bob)
121. We are still coupled to
doctrine for our persistence
122. class DoctrineUserRepository
{
//[..]
public function save(User $user)
{
$this->em->persist($user);
$this->em->flush();
}
public function find(int $userId) : User
{
return $this->em->find(User::class, $userId);
}
}
Repository
136. class DoctrineUserRepository implements UserRepositoryInterface
{
/**
* @throws DoctrineDBALExceptionConnectionException
*/
public function find(int $userId) : User;
}
class InMemoryUserRepository implements UserRepository
{
/**
* @throws RedisException
*/
public function find(int $userId) : User;
}
Repository
137. Donβt do this
class DoctrineUserRepository implements UserRepositoryInterface
{
/**
* @throws DoctrineDBALExceptionConnectionException
*/
public function find(int $userId) : User;
}
class InMemoryUserRepository implements UserRepository
{
/**
* @throws RedisException
*/
public function find(int $userId) : User;
}
Repository
138. class DoctrineUserRepository implements UserRepositoryInterface
{
public function find(UserId $userId) : User
{
try {
if ($user = $this->findById($userId)) {
return $user;
}
} catch (ConnectionException $e) {
throw ServiceUnavailableException::withOriginalException($e);
}
throw UserNotFoundException::withId($userId);
}
}
Normalize your exceptions
Repository
139. class DoctrineUserRepository implements UserRepositoryInterface
{
public function find(UserId $userId) : User
{
try {
if ($user = $this->findById($userId)) {
return $user;
}
} catch (ConnectionException $e) {
throw ServiceUnavailableException::withOriginalException($e);
}
throw UserNotFoundException::withId($userId);
}
}
Normalize your exceptions
The implementor now only has to worry about the exceptions
defined in the interface
Repository
140. The promise of a repository interface is now clear, simple and
implementation independent
143. src/UserBundle/
βββ Command
βββ Controller
βββ Entity
βββ Event
βββ Form
βββ Notifier
βββ Repository
βββ ValueObject
Letβs look at the structure
The domain, infrastructure and application are all
mixed in the bundle
159. There was 1 error:
1) UserDomainModelUserRegisterUserTest::can_register_user
UserDomainModelExceptionUserNotFoundException: User with id 1 not found
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
175. But a Uuid can still be any id
We can be more explicit
176. final class UserId // Simply wrapper of Uuid
{
public static function createNew() : self
/**
* @throws InvalidArgumentException
*/
public static function fromString(string $id) : self
public function toString() : string;
}
Value object
177. Now we know exactly what
we are talking about
178. public function can_register_user()
{
$id = UserId::createNew();
$this->registerUserHandler->handle(
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'Aart Staartjes')
);
$user = $this->inMemoryUserRepository->find($id);
// Assertions
}
179. phpunit --bootstrap vendor/autoload.php src/JO/User/
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.
.
Time: 266 ms, Memory: 5.25Mb
OK (1 test, 4 assertions)
205. Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
Value objects
What did we learn?
206. Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
Value objects
Decoupling from the framework
What did we learn?
207. What did we learn?
Writing fast tests (mocked environment)
208. What did we learn?
Writing fast tests (mocked environment)
Commands
209. What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
210. What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
211. What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front id generation strategy
212. What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front id generation strategy
Creating powerful domain exceptions
213. What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front id generation strategy
Creating powerful domain exceptions
Liskov substitution principle
215. We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
216. We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We
217. We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We
used commands to communicate with a unified
voice. Created domain events to allow for extension
218. We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We
used commands to communicate with a unified
voice. Created domain events to allow for extension
without cluttering the existing code. We end up with
219. We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We
used commands to communicate with a unified
voice. Created domain events to allow for extension
without cluttering the existing code. We end up with
clear, maintainable and beautiful software.
220. We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We
used commands to communicate with a unified
voice. Created domain events to allow for extension
without cluttering the existing code. We end up with
clear, maintainable and beautiful software.
That keeps us excited!
We are going to walk through the process of building an application.
We will start with some poor decisions and improve bit by bit and explaining the choices I make
Show techniques to decrease technical dept, improve code readability and maintainability
So that we end up with beautiful software
This is how every projects starts. A lot of excitement.
A clean sheet
This time Iβm going to do everything the right way
At the start the amount of progress is huge
New features are added rapidly
Then it slows down
And after a year you are barely moving forward
You slow down due to the increase of technical dept
Technical debt will increase over time but your goal is to have a linear increase
You want to prevent the exponential growth in technical debt
You fix one thing and break two
Letβs prevent an exponential growth in technical depth
Lack of intention - You donβt immediately see what a piece of code is about
Lack of intention - You donβt immediately see what a piece of code is about
Heavy coupling - to delivery mechanisms like http and persistence mechanisms like MySQL en MongoDB
Anemic domain models - Important models for the business that contain no behaviour and no validation
We start with a requirement
We create our user model with getters and setters
We make a controller we check if the form is submitted and valid.
We then persist the data.
That one requirement handled.
We require the name and email in the constructor.
We then validate that they are not empty.
The customer comes in with the next requirement
We expand our email validation so that it also checks for the email format.
Itβs becoming messy already
To solve this we can use a value object
Itβs becoming messy already
To solve this we can use a value object
We can create a value object like this
The constructor simply takes the argument as a string
We validate the the email is not empty
And that the email is in a valid format
And only then we assign the email to the user
Add a toString got retrieve the emailaddress
And we can add a check to check if two email addresses are the same.
The user can simply accept an email without worrying about validation
In the register user action we used to do $form->getData() to get a user
We now have to pass the required arguments to the user __construction
Our User is now always in a valid state
It looks like a small step but we made huge progress.
This means we can simplify other parts of the application
If we can not rely on the User to be in a valid state we place validation all over our application
Because we force our model to always be in a valid state
We can simplify this code
We can now just send our e-mail
Equality is based on value
The one 10 euro bill is the same as the other
It can still be different bills
If we were to create something for a bank, and care about each individual bill, then it would not be a value object.
The user always has a name and a valid e-mail
Everything clear?
Do you also know how to cope with form validation?
This is our current implementation, directly using the doctrine entity manager
We can simplify the controller and just call the save method
Render template
Create message
Send an email
The controller is rendering templates, creating messages and sending e-mails
We can easily extract the notifying
We separate this into itβs own class
We can now easily test it
We can now simply call the notifier.
The rendering, creating and sending of the mail message will be handled in the notifier.
Persistence is handled by the repository
And sending a confirmation message by the notifier
We create a new api controller
With a register action
That returns a JSON response
We get the request data
Create and save the user
Notify the user
and return a JsonResponse
We are duplicating the registration of a user
This will become problematic when adding more ways to register a user
or when adding new requirements to the registration
One simple command to register a new user
One simple handler
Creates the user
Saves the user
And notifies the user
One simple handler
Creates the user
Saves the user
And notifies the user
One simple handler
Creates the user
Saves the user
And notifies the user
One simple handler
Creates the user
Saves the user
And notifies the user
The web controller is now simply calling RegisterUserHandler::handle
We now always register a user in the same way
The command is always ignorant of the delivery mechanism.
If we want to add an auto generated password we only have to add it in the handler
There is always one to one relation
A command bus is a generic command handler
It receives a command and routes it to the handler
It provides the ability to add middleware
A command bus is a generic command handler
It receives a command and routes it to the handler
It provides the ability to add middleware
A command bus is a generic command handler
It receives a command and routes it to the handler
It provides the ability to add middleware
A command bus is a generic command handler
It receives a command and routes it to the handler
It provides the ability to add middleware
A command bus is a generic command handler
It receives a command and routes it to the handler
It provides the ability to add middleware
You can easily add logging to log every command in your application
We are still sending a confirmation message from within the handler
A simple object with the changed data
The ContainsRecordedMessages interface tells that we expose events.
The PrivateMessageRecorderCapabilities is a trait that allows us to record events and exposes them
The User::register method is an explicit way of creating a user. It also takes care of recording the event for us.
We can now let the eventbus publish all recorded messages
Handler publishes the event, the notifier and the logger listen
Some characteristics
We started with a data structure but we now have an object with meaningful behaviour
In our domain we have our UserRepository that uses doctrine to persist the user.
Our RegisterUserHandler shouldnβt deal with low level persistence
Our RegisterUserHandler shouldnβt deal with low level persistence
We can now rely on the interface instead of the concrete doctrine implementation
We are injecting the repository in the RegisterUserHandler
The RegisterUserHandler lets the repository handle the persistence
We donβt rely on low level details
We are injecting the repository in the RegisterUserHandler
The RegisterUserHandler lets the repository handle the persistence
We donβt rely on low level details
We are injecting the repository in the RegisterUserHandler
The RegisterUserHandler lets the repository handle the persistence
We donβt rely on low level details
Small step with huge benefits
Small step with huge benefits
Small step with huge benefits
The one relying on your interface only knows about the exceptions defined in that interface.It can not cope with every possible implementation
So this is the entire structure
The red classes are part of our domain. They contain the things that have the most value for our business.
They contain the business rules
The blue parts are infrastructural concerns.
The green parts are part of the application
The highlighted classes are part of our domain. The framework we happen to use is not important here
The highlighted classes are part of our infrastructure. The framework we happen to use is not important here
The highlighted classes are part of our application. The framework we happen to use is not important here
We are also less coupled to the framework.
A switch of framework or using a different framework for an api or microservice is now easier.
We are using a structure independent of the framework
I agree with this, because testable code is not always good code. But untestable code is almost always bad code.
We now require the persistence layer to handle id generation
And we donβt know the id until the user is registered
When using a command we will never know the id
There is still a problem
We are using generic exceptions that are used by almost any library
We can catch the very specific exception
We can catch the general domain exception
We can now catch the more explicit exceptions
We used the hexagonal architecture consisting of the inside and the outside
The application and the domain
The hexagonal architecture is also called ports and adapters
The number of ports on a hexagon is six. Six is an arbitrary number and not important here.
The domain doesnβt know anything about the delivery or persistence mechanisms
It contains our most import things, our business rules
Please give me some feedback.
Tell me what you liked and things I can improve
Clean code, DDD in PHP, Implement domain driven design
Carlos, the author of DDD in PHP is also present the Dutch PHP conference