+ All Categories
Home > Software > Crafting beautiful software

Crafting beautiful software

Date post: 14-Apr-2017
Category:
Upload: jorn-oomen
View: 1,266 times
Download: 0 times
Share this document with a friend
224
Crafting beautiful software
Transcript
Page 1: Crafting beautiful software

Crafting beautiful software

Page 2: Crafting beautiful software
Page 3: Crafting beautiful software

The start

Page 4: Crafting beautiful software

After a while

Page 5: Crafting beautiful software

A year later

Page 6: Crafting beautiful software

Source: fideloper.com/hexagonal-architecture

Page 7: Crafting beautiful software

Let’s prevent an exponential growth in technical debt

Page 8: Crafting beautiful software

Problems with the PHP industry standard

Page 9: Crafting beautiful software

Problems with the PHP industry standard

Lack of intention

Page 10: Crafting beautiful software

Problems with the PHP industry standard

Lack of intentionHeavy coupling

Page 11: Crafting beautiful software

Problems with the PHP industry standard

Lack of intentionHeavy coupling

Anemic domain models

Page 12: Crafting beautiful software

$ whoamiJorn OomenFreelance PHP Web developerGood weather cyclistlinkedin.com/in/jornoomen@jornoomen

Page 13: Crafting beautiful software

Let’s craft beautiful software

Page 14: Crafting beautiful software

RequirementA user needs to be registered

Page 15: Crafting beautiful software

class User{ private $id; private $name; private $email;

// Getters and setters}

Model

Page 16: Crafting beautiful software

public function registerAction(Request $request) : Response{ $form = $this->formFactory->create(UserType::class); $form->handleRequest($request); if ($form->isValid()) { /** @var User $user */ $user = $form->getData(); $this->em->persist($user); $this->em->flush(); }

return new Response(/**/);}

Controller

Page 17: Crafting beautiful software

RequirementA user has to have a name and an email

Page 18: Crafting beautiful software

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

Page 19: Crafting beautiful software

RequirementA user needs a valid email

Page 20: Crafting beautiful software

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

Page 21: Crafting beautiful software

It’s becoming messy already

Page 22: Crafting beautiful software

It’s becoming messy alreadyA case for the value object

Page 23: Crafting beautiful software

final class EmailAddress{

}

Value object

Page 24: Crafting beautiful software

final class EmailAddress{ public function __construct(string $email) {

}

}

Value object

Page 25: Crafting beautiful software

final class EmailAddress{ public function __construct(string $email) { if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); }

}

}

Value object

Page 26: Crafting beautiful software

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

Page 27: Crafting beautiful software

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

Page 28: Crafting beautiful software

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

Page 29: Crafting beautiful software

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

Page 30: Crafting beautiful software

class User{ public function __construct(EmailAddress $email, string $name) { $this->setEmail($email); $this->setName($name); } //[..]}

Model

Page 31: Crafting beautiful software

// Before$user = $form->getData();

Controller

Page 32: Crafting beautiful software

// Before$user = $form->getData();

//After$data = $form->getData();$user = new User(new EmailAddress($data['email']), $data['name'])

Controller

Page 33: Crafting beautiful software

Recap

Page 34: Crafting beautiful software

RecapEmail validation is handled by the value object

Page 35: Crafting beautiful software

RecapEmail validation is handled by the value object

Name and email are required constructor arguments

Page 36: Crafting beautiful software

RecapEmail validation is handled by the value object

Name and email are required constructor argumentsThe User model is always in a valid state

Page 37: Crafting beautiful software

if (!empty($user->getEmail())) { if (filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) { //Send an email }}

Before

Page 38: Crafting beautiful software

//Send an email

After

Page 39: Crafting beautiful software

“A small simple object, like money or a date range, whose equality isn't based on identity.”

Martin Fowler

Page 40: Crafting beautiful software

Value objectsSmall logical concepts

Contain no identityAre immutable

Equality is based on value

Page 41: Crafting beautiful software

==

Page 42: Crafting beautiful software
Page 43: Crafting beautiful software

$fiveEur = \Money\Money::EUR(500);$tenEur = $fiveEur->add($fiveEur);echo $fiveEur->getAmount(); // outputs 500echo $tenEur->getAmount(); // outputs 1000

Immutability example

Page 44: Crafting beautiful software

We have now enforced our business rules

Page 45: Crafting beautiful software

Everything clear?

Page 46: Crafting beautiful software

Some observationWe are now directly using doctrine for persistence

Page 47: Crafting beautiful software

Some observationWe are now directly using doctrine for persistence

A change of persistence would mean changing every class where we save or retrieve the user

Page 48: Crafting beautiful software

public function registerAction(Request $request) : Response{ // [..] $user = new User(new EmailAddress($data['email']), $data['name']); $this->em->persist($user); $this->em>flush();}

Controller

Page 49: Crafting beautiful software

Let’s move the persistence out of the controller

Page 50: Crafting beautiful software

class DoctrineUserRepository{ //[..] public function save(User $user) { $this->em->persist($user); $this->em->flush(); }}

Repository

Page 51: Crafting beautiful software

public function registerAction(Request $request) : Response{ // [..] $user = new User(new EmailAddress($data['email']), $data['name']); $this->userRepository->save($user);}

Controller

Page 52: Crafting beautiful software

Doctrine is great but we don’t want to marry it

A switch of persistence can be done by changing a single class

Page 53: Crafting beautiful software

RequirementA user needs to receive a registration confirmation

Page 54: Crafting beautiful software

public function registerAction(Request $request){ if ($form->isValid()) { // [..] $content = $this->renderTemplate(); $message = $this->createMailMessage($content, $user); $this->mailer->send($message); } }

Controller

Page 55: Crafting beautiful software

The controller is getting too FAT

Page 56: Crafting beautiful software

Let’s move the notifying out of the controller

Page 57: Crafting beautiful software

class UserRegisteredNotifier{ //[..] public function notify(string $email, string $name) { $content = $this->renderTemplate(); $message = $this->createMailMessage($content, $email, $name); $this->mailer->send($message); }}

Notifier

Page 58: Crafting beautiful software

public function registerAction(Request $request){ if ($form->isValid()) { // [..] $this->userRegisteredNotifier->notify($data['email'], $data['name']); }}

Controller

Page 59: Crafting beautiful software

So this looks better already

Page 60: Crafting beautiful software

Everything clear?

Page 61: Crafting beautiful software

RequirementUser registration has to be available through an API

Page 62: Crafting beautiful software

public function registerAction(Request $request) : JsonResponse{

}

Controller

Page 63: Crafting beautiful software

public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);

}

Controller

Page 64: Crafting beautiful software

public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);

$user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user);

}

Controller

Page 65: Crafting beautiful software

public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);

$user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user);

$this->userRegisteredNotifier->notify($data['email'], $data['name']);

}

Controller

Page 66: Crafting beautiful software

public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);

$user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user);

$this->userRegisteredNotifier->notify($data['email'], $data['name']);

return new JsonResponse(['id' => $user->getId()]);}

Controller

Page 67: Crafting beautiful software
Page 68: Crafting beautiful software

$this->userRepository->save($user);

$this->userRegisteredNotifier->notify($data['email'], $data['name']);

Controller

Page 69: Crafting beautiful software

Introducing the command pattern

Page 70: Crafting beautiful software

class RegisterUser // A command always has a clear intention{ public function __construct(string $email, string $name) { $this->email = $email; $this->name = $name; } // Getters}

Command

Page 71: Crafting beautiful software

class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) {

}}

Command handler

Page 72: Crafting beautiful software

class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName());

}}

Command handler

Page 73: Crafting beautiful software

class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName()); $this->userRepository->save($user);

}}

Command handler

Page 74: Crafting beautiful software

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

Page 75: Crafting beautiful software

public function registerAction(Request $request) : Response{ if ($form->isValid()) { // [..] $data = $form->getData(); $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) ); }}

Controller

Page 76: Crafting beautiful software

public function registerAction(Request $request) : JsonResponse{ // [..] $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) );

return new JsonResponse([]);}

Controller

Page 77: Crafting beautiful software
Page 78: Crafting beautiful software
Page 79: Crafting beautiful software

Commands

Page 80: Crafting beautiful software

CommandsOnly contain a message

Page 81: Crafting beautiful software

CommandsOnly contain a message

Have a clear intention (explicit)

Page 82: Crafting beautiful software

CommandsOnly contain a message

Have a clear intention (explicit)Are immutable

Page 83: Crafting beautiful software

CommandsOnly contain a message

Have a clear intention (explicit)Are immutable

Command handlers never return a value

Page 84: Crafting beautiful software

Commands and the command bus

Page 85: Crafting beautiful software

A command bus is a generic command handler

Commands and the command bus

Page 86: Crafting beautiful software

Commands and the command bus

A command bus is a generic command handlerIt receives a command and routes it to the handler

Page 87: Crafting beautiful software

Commands and the command bus

A command bus is a generic command handlerIt receives a command and routes it to the handler

It provides the ability to add middleware

Page 88: Crafting beautiful software

A great command bus implementation

github.com/SimpleBus/MessageBus

Page 89: Crafting beautiful software
Page 90: Crafting beautiful software

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

Page 91: Crafting beautiful software

public function handle($command, callable $next){ if ($this->canBeDelayed($command)) { $this->commandQueue->add($command); } else { $next($command); }}

Queueing middleware example

Page 92: Crafting beautiful software
Page 93: Crafting beautiful software

//Before$this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']));

//After$this->commandBus->handle( new RegisterUser($data['email'], $data['name']));

Controller

Page 94: Crafting beautiful software

The command busProvides the ability to add middleware

Page 95: Crafting beautiful software

The command busProvides the ability to add middleware

Now logs every command for us

Page 96: Crafting beautiful software

The command busProvides the ability to add middleware

Now logs every command for usAllows queueing of (slow) commands

Page 97: Crafting beautiful software

Everything clear?

Page 98: Crafting beautiful software

The handler is still dealing with secondary concerns

Page 99: Crafting beautiful software

Introducing domain events

Page 100: Crafting beautiful software

class UserIsRegistered // An event tells us what has happened{ public function __construct(int $userId, string $emailAddress, string $name) {} // Getters}

Event

Page 101: Crafting beautiful software

class User implements ContainsRecordedMessages{

//[..]}

Model

Page 102: Crafting beautiful software

class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;

//[..]}

Model

Page 103: Crafting beautiful software

class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;

public static function register(EmailAddress $email, string $name) : self {

}

//[..]}

Model

Page 104: Crafting beautiful software

class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;

public static function register(EmailAddress $email, string $name) : self { $user = new self($email, $name);

}

//[..]}

Model

Page 105: Crafting beautiful software

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

Page 106: Crafting beautiful software

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

Page 107: Crafting beautiful software

class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { // save user foreach ($user->recordedMessages() as $event) { $this->eventBus->handle($event); } }}

Command handler

Page 108: Crafting beautiful software

class NotifyUserWhenUserIsRegistered{ //[..] public function handle(UserIsRegistered $userIsRegistered) { $this->userRegisteredNotifier->notify($userIsRegistered->getEmail(), $userIsRegistered->getName()); }}

Event listener

Page 109: Crafting beautiful software
Page 110: Crafting beautiful software

Domain events

Page 111: Crafting beautiful software

Domain eventsAre in past tense

Page 112: Crafting beautiful software

Domain eventsAre in past tense

Are always immutable

Page 113: Crafting beautiful software

Domain eventsAre in past tense

Are always immutableCan have zero or more listeners

Page 114: Crafting beautiful software

So we are pretty happy nowController creates simple command

Mailing doesn’t clutter our code

Page 115: Crafting beautiful software

We now have a rich user model

Page 116: Crafting beautiful software

We now have a rich user modelIt contains data

Page 117: Crafting beautiful software

We now have a rich user modelIt contains data

It contains validation

Page 118: Crafting beautiful software

We now have a rich user modelIt contains data

It contains validationIt contains behaviour

Page 119: Crafting beautiful software

“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)

Page 120: Crafting beautiful software

Everything clear?

Page 121: Crafting beautiful software

We are still coupled to doctrine for our persistence

Page 122: Crafting beautiful software

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

Page 123: Crafting beautiful software

We shouldn’t depend on any persistence implementation

Page 124: Crafting beautiful software

We shouldn’t depend on any persistence implementation

A case for the dependency inversion principle

Page 125: Crafting beautiful software

interface UserRepository{ public function save(User $user);

public function find(int $userId) : User;}

Repository

Page 126: Crafting beautiful software

class DoctrineUserRepository implements UserRepositoryInterface{ //[..]}

Repository

Page 127: Crafting beautiful software

class RegisterUserHandler{ public function __construct(UserRepositoryInterface $userRepository, MessageBus $eventBus) { //[..] }}

Command handler

Page 128: Crafting beautiful software

DI: Dependency injection

Our situation

Page 129: Crafting beautiful software

DI: Dependency injectionIoC: Inversion of control

Our situation

Page 130: Crafting beautiful software

DI: Dependency injectionIoC: Inversion of controlDIP: Dependency inversion principle

Our situation

Page 131: Crafting beautiful software

Tests - InMemoryUserRepository

Decoupling provides options

Page 132: Crafting beautiful software

Tests - InMemoryUserRepositoryDevelopment - MysqlUserRepository

Decoupling provides options

Page 133: Crafting beautiful software

Tests - InMemoryUserRepositoryDevelopment - MysqlUserRepositoryProduction - WebserviceUserRepository

Decoupling provides options

Page 134: Crafting beautiful software

Be clear about your exceptions

Some note

Page 135: Crafting beautiful software

interface UserRepository{ /** * @throws UserNotFoundException * @throws ServiceUnavailableException */ public function find(int $userId) : User;

//[..]}

Repository

Page 136: Crafting beautiful software

class DoctrineUserRepository implements UserRepositoryInterface{ /** * @throws \Doctrine\DBAL\Exception\ConnectionException */ public function find(int $userId) : User;}class InMemoryUserRepository implements UserRepository{ /** * @throws \RedisException */ public function find(int $userId) : User;}

Repository

Page 137: Crafting beautiful software

Don’t do thisclass DoctrineUserRepository implements UserRepositoryInterface{ /** * @throws \Doctrine\DBAL\Exception\ConnectionException */ public function find(int $userId) : User;}class InMemoryUserRepository implements UserRepository{ /** * @throws \RedisException */ public function find(int $userId) : User;}

Repository

Page 138: Crafting beautiful software

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 exceptionsRepository

Page 139: Crafting beautiful software

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

Page 140: Crafting beautiful software

The promise of a repository interface is now clear, simple and implementation independent

Page 141: Crafting beautiful software

Everything clear?

Page 142: Crafting beautiful software

src/UserBundle/ ├── Command ├── Controller ├── Entity ├── Event ├── Form ├── Notifier ├── Repository └── ValueObject

Let’s look at the structure

Page 143: Crafting beautiful software

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

Page 144: Crafting beautiful software

src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php

Page 145: Crafting beautiful software

src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php

Domain

Page 146: Crafting beautiful software

src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php

DomainInfrastructure

Page 147: Crafting beautiful software

src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php

DomainInfrastructureApplication

Page 148: Crafting beautiful software

src/UserBundle/├── Command│ ├── RegisterUser.php│ └── RegisterUserHandler.php├── Controller│ ├── RegisterUserApiController.php│ └── RegisterUserController.php├── Entity│ ├── User.php│ └── UserRepository.php├── Event│ └── UserIsRegistered.php├── Form│ └── UserType.php├── Notifier│ ├── NotifyUserWhenUserIsRegistered.php│ └── UserRegisteredNotifier.php├── Repository│ └── DoctrineUserRepository.php├── Resources/config/doctrine│ └── User.orm.yml└── ValueObject └── EmailAddress.php

src/User/└── DomainModel └── User ├── EmailAddress.php ├── User.php ├── UserIsRegistered.php └── UserRepository.php

Page 149: Crafting beautiful software

src/UserBundle/├── Command│ ├── RegisterUser.php│ └── RegisterUserHandler.php├── Controller│ ├── RegisterUserApiController.php│ └── RegisterUserController.php├── Entity│ ├── User.php│ └── UserRepository.php├── Event│ └── UserIsRegistered.php├── Form│ └── UserType.php├── Notifier│ ├── NotifyUserWhenUserIsRegistered.php│ └── UserRegisteredNotifier.php├── Repository│ └── DoctrineUserRepository.php├── Resources/config/doctrine│ └── User.orm.yml└── ValueObject └── EmailAddress.php

src/User/├── DomainModel│ └── User│ ├── EmailAddress.php│ ├── User.php│ ├── UserIsRegistered.php│ └── UserRepository.php└── Infrastructure ├── Messaging/UserRegisteredNotifier.php └── Persistence ├── User │ └── DoctrineUserRepository.php └── config/doctrine └── User.User.orm.yml

Page 150: Crafting beautiful software

src/UserBundle/├── Command│ ├── RegisterUser.php│ └── RegisterUserHandler.php├── Controller│ ├── RegisterUserApiController.php│ └── RegisterUserController.php├── Entity│ ├── User.php│ └── UserRepository.php├── Event│ └── UserIsRegistered.php├── Form│ └── UserType.php├── Notifier│ ├── NotifyUserWhenUserIsRegistered.php│ └── UserRegisteredNotifier.php├── Repository│ └── DoctrineUserRepository.php├── Resources/config/doctrine│ └── User.orm.yml└── ValueObject └── EmailAddress.php

src/User/├── DomainModel│ └── User│ ├── EmailAddress.php│ ├── User.php│ ├── UserIsRegistered.php│ └── UserRepository.php├── Infrastructure│ ├── Messaging/UserRegisteredNotifier.php│ └── Persistence│ ├── User│ │ └── DoctrineUserRepository.php│ └── config/doctrine│ └── User.User.orm.yml└── Application ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Form/UserType.php └── Messaging/NotifyUserWhenUserIsRegistered.php

Page 151: Crafting beautiful software

We are looking pretty goodApplication, domain and infrastructural concerns are

separated.

Page 152: Crafting beautiful software

Everything clear?

Page 153: Crafting beautiful software

Let’s test this awesome software

Page 154: Crafting beautiful software
Page 155: Crafting beautiful software

public function can_register_user(){

}

Page 156: Crafting beautiful software

public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes'));

}

Page 157: Crafting beautiful software

public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1);

}

Page 158: Crafting beautiful software

public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1); $this->assertInstanceOf(User::class, $user); $this->assertEquals(new EmailAddress('[email protected]'), $user->getEmail()); $this->assertSame('Aart Staartjes', $user->getName()); $this->assertInstanceOf(UserIsRegistered::class, $this->eventBusMock->getRaisedEvents()[0]);}

Page 159: Crafting beautiful software

There was 1 error:

1) User\DomainModel\User\RegisterUserTest::can_register_userUser\DomainModel\Exception\UserNotFoundException: User with id 1 not found

FAILURES!Tests: 1, Assertions: 0, Errors: 1.

Page 160: Crafting beautiful software

public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1); $this->assertInstanceOf(User::class, $user); $this->assertEquals(new EmailAddress('[email protected]'), $user->getEmail()); $this->assertSame('Aart Staartjes', $user->getName()); $this->assertInstanceOf(UserIsRegistered::class, $this->eventBusMock->getRaisedEvents()[0]);}

Page 161: Crafting beautiful software

We rely on the magic of the persistence layer

Page 162: Crafting beautiful software

User provides identity - The email

Unique identity options

Page 163: Crafting beautiful software

User provides identity - The emailPersistence mechanism generates identity - Auto increment

Unique identity options

Page 164: Crafting beautiful software

User provides identity - The emailPersistence mechanism generates identity - Auto incrementApplication generates identity - UUID

Unique identity options

Page 165: Crafting beautiful software

Let’s remove the magicBy implementing an up front id generation strategy

Page 166: Crafting beautiful software

composer require ramsey/uuid

Page 167: Crafting beautiful software
Page 168: Crafting beautiful software

class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;

public static function register(UuidInterface $id, EmailAddress $email, string $name) : self { $user = new self($id, $email, $name); $user->record(new UserIsRegistered((string) $id, (string) $email, $name));

return $user; } //[..]}

Model

Page 169: Crafting beautiful software

public function can_register_user(){ $id = Uuid::uuid4();

}

Page 170: Crafting beautiful software

public function can_register_user(){ $id = Uuid::uuid4();

new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes')

}

Page 171: Crafting beautiful software

public function can_register_user(){ $id = Uuid::uuid4(); $this->registerUserHandler->handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') );

}

Page 172: Crafting beautiful software

public function can_register_user(){ $id = Uuid::uuid4(); $this->registerUserHandler->handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') ); $user = $this->inMemoryUserRepository->find($id); // Assertions}

Page 173: Crafting beautiful software

phpunit --bootstrap=vendor/autoload.php test/PHPUnit 5.3.2 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 125 ms, Memory: 8.00Mb

OK (1 test, 4 assertions)

Page 174: Crafting beautiful software

Tested

Page 175: Crafting beautiful software

But a Uuid can still be any idWe can be more explicit

Page 176: Crafting beautiful software

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

Page 177: Crafting beautiful software

Now we know exactly what we are talking about

Page 178: Crafting beautiful software

public function can_register_user(){ $id = UserId::createNew(); $this->registerUserHandler->handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') ); $user = $this->inMemoryUserRepository->find($id); // Assertions}

Page 179: Crafting beautiful software

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)

Page 180: Crafting beautiful software

Everything clear?

Page 181: Crafting beautiful software

Let’s test our value objects

Page 182: Crafting beautiful software

public function can_not_create_invalid_user_id(){ $this->expectException(\InvalidArgumentException::class); UserId::fromString('invalid format');}

Page 183: Crafting beautiful software

public function can_not_create_invalid_email_address(){ $this->expectException(\InvalidArgumentException::class); new EmailAddress('invalid format');}

Page 184: Crafting beautiful software

PHPUnit 5.3.2 by Sebastian Bergmann and contributors.

... 3 / 3 (100%)

Time: 120 ms, Memory: 8.00Mb

Page 185: Crafting beautiful software

Great that’s testedBut..

Page 186: Crafting beautiful software

public function can_not_create_invalid_user_id(){ $this->expectException(\InvalidArgumentException::class); UserId::fromString('invalid format');}

public function can_not_create_invalid_email_address(){ $this->expectException(\InvalidArgumentException::class); new EmailAddress('invalid format');}

Page 187: Crafting beautiful software

$this->commandBus>handle( new RegisterUser('invalid id', 'invalid email', $name = '’) );

Page 188: Crafting beautiful software

We still have limited control over our exceptions

Useful domain exceptions can give us more control

Page 189: Crafting beautiful software

namespace User\DomainModel\Exception;

abstract class DomainException extends \DomainException {}

Page 190: Crafting beautiful software

namespace User\DomainModel\Exception;

abstract class DomainException extends \DomainException {}

class InvalidEmailAddressException extends DomainException {}

class InvalidUserIdException extends DomainException {}

class NoEmailAddressProvidedException extends DomainException {}

Page 191: Crafting beautiful software

try { $id = UserId::createNew(); $this->commandBus>handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') );} catch (DomainModel\Exception\InvalidEmailAddressException $e) { // Show invalid email error}

Page 192: Crafting beautiful software

try { $id = UserId::createNew(); $this->commandBus>handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') );} catch (DomainModel\Exception\InvalidEmailAddressException $e) { // Show invalid email error} catch (DomainModel\Exception\DomainException $e) { // Some domain exception occurred}

Page 193: Crafting beautiful software

public function can_not_create_invalid_user_id(){ $this->expectException(InvalidUserIdException::class); UserId::fromString('invalid format');}

public function can_not_create_invalid_email_address(){ $this->expectException(InvalidEmailAddressProvidedException::class); new EmailAddress('invalid format');}

Page 194: Crafting beautiful software

PHPUnit 5.3.2 by Sebastian Bergmann and contributors.

... 3 / 3 (100%)

Time: 122 ms, Memory: 8.00Mb

OK (3 tests, 6 assertions)

Page 195: Crafting beautiful software

Everything clear?

Page 196: Crafting beautiful software

So what did we create?

Page 197: Crafting beautiful software
Page 198: Crafting beautiful software
Page 199: Crafting beautiful software
Page 200: Crafting beautiful software
Page 201: Crafting beautiful software

Intention revealing code

What did we learn?

Page 202: Crafting beautiful software

Intention revealing codeTestable code

What did we learn?

Page 203: Crafting beautiful software

Intention revealing codeTestable code

Preventing the big ball of mud

What did we learn?

Page 204: Crafting beautiful software

Intention revealing codeTestable code

Preventing the big ball of mudAnemic domain models (anti pattern)

What did we learn?

Page 205: Crafting beautiful software

Intention revealing codeTestable code

Preventing the big ball of mudAnemic domain models (anti pattern)

Value objects

What did we learn?

Page 206: Crafting beautiful software

Intention revealing codeTestable code

Preventing the big ball of mudAnemic domain models (anti pattern)

Value objectsDecoupling from the framework

What did we learn?

Page 207: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

Page 208: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

Commands

Page 209: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

CommandsDomain Events

Page 210: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

CommandsDomain Events

Dependency inversion principle

Page 211: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

CommandsDomain Events

Dependency inversion principleUp front id generation strategy

Page 212: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

CommandsDomain Events

Dependency inversion principleUp front id generation strategy

Creating powerful domain exceptions

Page 213: Crafting beautiful software

What did we learn?Writing fast tests (mocked environment)

CommandsDomain Events

Dependency inversion principleUp front id generation strategy

Creating powerful domain exceptionsLiskov substitution principle

Page 214: Crafting beautiful software

We wrote intention revealing code. Separated the

Page 215: Crafting beautiful software

We wrote intention revealing code. Separated the domain, infrastructure and application. Created

Page 216: Crafting beautiful software

We wrote intention revealing code. Separated the domain, infrastructure and application. Created

abstractions to improve testability and flexibility. We

Page 217: Crafting beautiful software

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

Page 218: Crafting beautiful software

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

Page 219: Crafting beautiful software

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.

Page 220: Crafting beautiful software

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!

Page 221: Crafting beautiful software

Questions?Questions?

Page 222: Crafting beautiful software

Please rate!

joind.in/17557

Page 223: Crafting beautiful software

Further reading

Page 224: Crafting beautiful software

Used resourceshttp://www.slideshare.net/matthiasnoback/hexagonal-architecture-messageoriented-software-designhttps://www.youtube.com/watch?v=Eg6m6mU0fH0https://www.youtube.com/watch?v=mQsQ6QZ4dGghttps://kacper.gunia.me/blog/ddd-building-blocks-in-php-value-objecthttp://williamdurand.fr/2013/12/16/enforcing-data-encapsulation-with-symfony-forms/http://simplebus.github.io/MessageBus/http://php-and-symfony.matthiasnoback.nl/2014/06/don-t-use-annotations-in-your-controllers/http://alistair.cockburn.us/Hexagonal+architecturehttp://www.slideshare.net/cakper/2014-0407-php-spec-the-only-design-tool-you-need-4developers/117-enablesRefactoring


Recommended