Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Domain-Driven Design - Tactical Design Patterns

Christian
October 29, 2014

Domain-Driven Design - Tactical Design Patterns

The slides for a talk I gave for the Software Craftsmanship Barcelona 2014 about Domain-Driven Design (DDD) on the main Tactical Design Patterns with examples written in PHP

Christian

October 29, 2014
Tweet

More Decks by Christian

Other Decks in Technology

Transcript

  1. Domain-Driven Design
    Tactical Design Patterns

    View Slide

  2. Christian Soronellas
    Extreme Programmer
    Technology Enthusiast
    Web Developer

    View Slide

  3. View Slide

  4. Agenda
    Tactical Design Patterns I Tactical Design Patterns II
    Value Objects
    Entities
    Domain Services
    Repositories
    Domain Events
    Final Thoughts

    View Slide

  5. Value Objects

    View Slide

  6. First defined by Ward Cunningham as
    Whole Value

    View Slide

  7. Also defined in Martin’s Fowler Patterns of
    Enterprise Application Architecture

    View Slide

  8. “Beside using the handful of literal values offered by
    the language, and an even smaller complement of
    objects normally used as values, you will make and
    use new objects that represent the meaningful
    quantities of your business”

    View Slide

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

    View Slide

  10. class Currency {

    private $isoCode;


    public function __construct($isoCode) {

    $this->isoCode = $isoCode;

    }


    public function isoCode() {

    return $this->isoCode;

    }

    }

    View Slide

  11. class Money {

    private $amount;

    private $currency;


    public function __construct($amount, Currency $currency) {

    $this->amount = $amount;

    $this->currency = $currency;

    }


    public function amount() {

    return $this->amount;

    }


    public function currency() {

    return $this->currency;

    }

    }

    View Slide

  12. They mesure, quantify or describe things

    View Slide

  13. They are immutable

    View Slide

  14. class Money {

    // ...


    public function increaseBy(Money $aQuantity) {

    $this->assertMoneyHasSameCurrency($aQuantity);


    return new self(

    $this->amount + $aQuantity->amount,

    $this->currency

    );

    }

    }

    View Slide

  15. They promote side-effect free functions

    View Slide

  16. class Money {

    // ...


    public function increaseBy(Money $aQuantity) {

    $this->assertMoneyHasSameCurrency($aQuantity);


    return new self(

    $this->amount + $aQuantity->amount,

    $this->currency

    );

    }

    }

    View Slide

  17. They promote closure of operations

    View Slide

  18. class Money {

    // ...


    public function increaseBy(Money $aQuantity) {

    $this->assertMoneyHasSameCurrency($aQuantity);


    return new self(

    $this->amount + $aQuantity->amount,

    $this->currency

    );

    }

    }

    View Slide

  19. They form conceptual wholes

    View Slide

  20. class PostalCode {

    private $postalCode;


    public function __construct($aPostalCode) {

    $this->postalCode = $aPostalCode;

    }

    }

    View Slide

  21. class City {

    private $city;


    public function __construct($aCityName) {

    $this->city = $aCityName;

    }

    }

    View Slide

  22. class Address {

    private $street;

    private $postalCode;

    private $city;


    public function __construct(

    $anStreet,

    PostalCode $aPostalCode,

    City $aCity

    ) {

    $this->street = $anStreet;

    $this->postalCode = $aPostalCode;

    $this->city = $aCity;

    }

    }

    View Slide

  23. They generally boost good object-oriented design
    by promoting proper encapsulation

    View Slide

  24. $amount1 = new Money(100, Currency::USD());

    $amount2 = new Money(100, Currency::EUR());


    false === assert(

    ($amount1->currency()->isoCode() === $amount2->currency()->isoCode())

    && ($amount1->amount() === $amount2->amount())

    );

    View Slide

  25. View Slide

  26. class Currency {

    // ...


    public function equalsTo(self $anotherCurrency) {

    return $this->isoCode === $anotherCurrency->isoCode;

    }

    }

    View Slide

  27. class Money {

    // ...


    public function equalsTo(self $anotherMoney) {

    return $this->currency->equalsTo($anotherMoney->currency)

    && $this->amount === $anotherMoney->amount;

    }

    }

    View Slide

  28. $amount1 = new Money(100, Currency::USD());

    $amount2 = new Money(100, Currency::EUR());


    false === assert($amount1->equalsTo($amount2));

    View Slide

  29. Use Embedded Value or Serialized LOB to persist Value
    Objects

    View Slide

  30. Entities

    View Slide

  31. Some concepts in the ubiquitous language may
    demand a thread of identity

    View Slide

  32. class Person {

    private $identificationNumber;

    private $name;


    public function __construct(

    $anIdentificationNumber,

    Name $aName

    ) {

    $this->identificationNumber = $anIdentificationNumber;

    $this->name = $aName;

    }


    public function identificationNumber()

    {

    return $this->identificationNumber;

    }


    public function name()

    {

    return $this->name;

    }

    }

    View Slide

  33. class Order {

    private $id;

    private $amount;


    public function __construct($anId, Amount $amount) {

    $this->id = $anId;

    $this->amount = $amount;

    }


    public function id() {

    return $this->id;

    }


    public function amount() {

    return $this->amount;

    }

    }

    View Slide

  34. Model the entity identity as a an object rather

    than a primitive type

    View Slide

  35. class OrderId {

    private $id;


    public function __construct($anId) {

    $this->id = $anId;

    }


    public function id() {

    return $this->id;

    }


    public function equalsTo(self $anOrderId) {

    return $anOrderId->id === $this->id;

    }

    }

    View Slide

  36. The identity operation

    View Slide

  37. Persistence mechanism provides identity

    View Slide

  38. CREATE TABLE `orders` (

    `id` INT(11) NOT NULL AUTO_INCREMENT,

    `amount` DECIMAL(10, 5) NOT NULL,

    PRIMARY KEY (`id`)

    ) ENGINE=InnoDB;

    View Slide

  39. $anOrder = new Order(/* ... */);

    assert(null === $anOrder->id());

    View Slide

  40. $entityManager->persist($anOrder);

    $entityManager->flush();


    assert(null !== $anOrder->id());

    View Slide

  41. Client provides identity

    View Slide

  42. EAN13

    View Slide

  43. ISBN

    View Slide

  44. class ISBN {

    private $isbn;


    public function __construct($anIsbn) {

    $this->isbn = $anIsbn;

    }


    public function isbn() {

    return $this->isbn;

    }

    }

    View Slide

  45. class Book {

    private $isbn;

    private $title;


    public function __construct(ISBN $anIsbn, $aTitle) {

    $this->isbn = $anIsbn;

    $this->title = $aTitle;

    }

    }

    View Slide

  46. $book = new Book(

    ISBN::create('978-0-300-14424-6'),

    'Domain-Driven Design with PHP by Examples'

    );

    View Slide

  47. Application provides identity

    View Slide

  48. UUID

    (Universally unique identifier)

    View Slide

  49. https://github.com/ramsey/uuid

    View Slide

  50. use Rhumsaa\Uuid\UUID;


    $order = new Order(

    OrderId::create(UUID::uuid4()),

    new Amount(100, Currency::EUR())

    );

    View Slide

  51. Validating entities

    View Slide

  52. State should be protected using a 

    design-by-contract approach.

    View Slide

  53. class EmailAddress {

    private $address;


    public function __construct($anAddress) {

    $this->setAddress($anAddress);

    }


    private function setAddress($anAddress) {

    $this->assertNotNull($anAddress);

    $this->assertLengthGreaterThan($anAddress, 0);

    $this->assertLengthLowerThan($anAddress, 100);

    $this->assertIsValidEmailAddress($anAddress);


    $this->address = $anAddress;

    }

    }

    View Slide

  54. class EmailAddress {

    // ...


    private function assertNotNull($anAddress) {

    if (null === $anAddress) {

    throw new InvalidArgumentException();

    }

    }

    }

    View Slide

  55. class EmailAddress {

    // ...


    private function assertLengthGreaterThan($anAddress, $min) {

    if (strlen($anAddress) <= $min) {

    throw new InvalidArgumentException();

    }

    }

    }

    View Slide

  56. class EmailAddress {

    // ...


    private function assertLengthLowerThan($anAddress, $max) {

    if (strlen($anAddress) >= $max) {

    throw new InvalidArgumentException();

    }

    }

    }

    View Slide

  57. class EmailAddress {

    // ...


    private function assertIsValidEmailAddress($anAddress) {

    if (false === filter_var($anAddress, FILTER_VALIDATE_EMAIL)) {

    throw new InvalidArgumentException();

    }

    }

    }

    View Slide

  58. Sometimes even having all properties valid, the object
    could still be in an inconsistent state.

    View Slide

  59. class Location {

    private $country;

    private $city;

    private $postalCode;


    public function __construct(Country $country, City $city, PostalCode $postalCode) {

    $this->setCountry($country);

    $this->setCity($city);

    $this->setPostalCode($postalCode);

    }


    public function country() {

    return $this->country;

    }


    public function city() {

    return $this->city;

    }


    public function postalCode() {

    return $this->postalCode;

    }


    // ...

    }

    View Slide

  60. abstract class Validator {

    private $validationHandler;


    public function __construct(ValidationHandler $validationHandler) {

    $this->validationHandler = $validationHandler;

    }


    protected function handleError($error) {

    $this->validationHandler->handleError($error);

    }


    abstract public function validate($aDomainObject);

    }

    View Slide

  61. class LocationValidator extends Validator {


    public function validate($aLocation) {

    $aCity = $aLocation->city();

    $aPostalCode = $aLocation->postalCode();


    if (!$this->location->getCountry()->hasCity($aCity)) {

    $this->handleError('City not found!');

    }


    if (!$this->location->getCity()->isPostalCodeValid($aPostalCode)) {

    $this->handleError('Invalid postal code!');

    }

    }


    }

    View Slide

  62. Persisting Entities

    View Slide

  63. Active Record vs Data Mapper

    View Slide

  64. Active Record example

    View Slide

  65. class Order extends Eloquent {

    protected $table = 'orders';

    }

    View Slide

  66. // Find all orders

    $orders = Order::all();


    // Find an specific Order

    $anOrder = Order::find(1);


    // Find all orders whose amount is greater than 100 EUR

    $orders = Order::where('amount', '>', 100)->take(10)->get();


    // Create and save a new order

    $aNewOrder = Order::create(/* ... */);

    $aNewOrder->save();

    View Slide

  67. An Active Record is fine mostly for CRUD applications

    or for simple domains

    View Slide

  68. Active Record couples the design of the database to the
    design of the object-oriented system

    View Slide

  69. Advanced things like collections or inheritance are tricky to
    implement

    View Slide

  70. Most of the implementations force the use, through
    inheritance, of some sort of constructions
    to impose their conventions.

    View Slide

  71. class Order extends Eloquent {


    protected $table = 'orders';


    }

    View Slide

  72. Data Mapper example

    View Slide

  73. class Order {

    private $id;

    private $amount;


    public function __construct(OrderId $id, Amount $amount) {

    $this->id = $id;

    $this->amount = $amount;

    }


    public function id() {

    return $this->id;

    }


    public function amount() {

    return $this->amount;

    }

    }

    View Slide

  74. class Order {

    private $id;

    private $amount;


    public function __construct(OrderId $id, Amount $amount) {

    $this->id = $id;

    $this->amount = $amount;

    }


    public function id() {

    return $this->id;

    }


    public function amount() {

    return $this->amount;

    }

    }

    View Slide

  75. $orderMapper = new OrderMapper($databaseConnection);


    $order = new Order(/* ... */);


    $orderMapper->save($order);

    View Slide

  76. Data Mapper promotes real separation of concerns 

    by decoupling persistence from

    domain concerns.

    View Slide

  77. Surrogate Identity

    View Slide

  78. abstract class IdentifiableDomainObject {

    private $id;


    protected function id() {

    return $this->id;

    }


    protected function setId($anId) {

    $this->id = $anId;

    }

    }

    View Slide

  79. class Order extends IdentifiableDomainObject {

    private $orderId;


    public function orderId() {

    if (null === $this->orderId) {

    $this->orderId = new OrderId($this->id());

    }


    return $this->orderId;

    }

    }

    View Slide

  80. Services

    View Slide

  81. Stateless operations that satisfy a domain task

    View Slide

  82. Usually they are all those operations that don’t
    fit naturally in an aggregate nor in
    a value object.

    View Slide

  83. A user authenticates itself?

    View Slide

  84. A cart is promoted as an order by itself when the
    purchase is made?

    View Slide

  85. class AuthenticationService {

    // ...


    public function authenticate($aUsername, $aPassword) {

    // ...

    }

    }

    View Slide

  86. class PurchaseService {

    // ...


    public function createOrderFrom(Cart $aCart) {

    // ...

    }

    }

    View Slide

  87. Anemic domain models

    vs.

    Rich domain models

    View Slide

  88. CREATE TABLE `orders` (

    `ID` INTEGER NOT NULL AUTO_INCREMENT,

    `CUSTOMER_ID` INTEGER NOT NULL,

    `AMOUNT` DECIMAL(17, 2) NOT NULL DEFAULT '0.00',

    `STATUS` TINYINT NOT NULL DEFAULT 0,

    `CREATED_AT` DATETIME NOT NULL,

    `UPDATED_AT` DATETIME NOT NULL,

    PRIMARY KEY (`ID`)

    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATION;

    View Slide

  89. class Order {

    // ...


    public function setId($id){$this->id = $id;}

    public function getId(){return $this->id;}


    public function setCustomerId($customerId){$this->customerId = $customerId;}

    public function getCustomerId(){return $this->customerId;}


    public function setAmount($amount){$this->amount = $amount;}

    public function getAmount(){return $this->amount;}


    public function setStatus($status){$this->status = $status;}

    public function getStatus(){return $this->status;}


    public function setCreatedAt($createdAt){$this->createdAt = $createdAt;}

    public function getCreatedAt(){return $this->createdAt;}


    public function setUpdatedAt($updatedAt){$this->updatedAt = $updatedAt;}

    public function getUpdatedAt(){return $this->updatedAt;}

    }

    View Slide

  90. // fetch an order from the database

    $anOrder = $orderRepository->find(1);


    // Update order status

    $anOrder->setStatus(Order::STATUS_ACCEPTED);


    // Update updatedAt field

    $anOrder->setUpdatedAt(new DateTimeImmutable());


    // Save the order to the database

    $orderRepository->save($anOrder);

    View Slide

  91. class ChangeOrderStatusService {

    private $orderRepository;


    public function __construct(OrderRepository $orderRepository) {

    $this->orderRepository = $orderRepository;

    }


    public function execute($anOrderId, $anOrderStatus) {

    // fetch an order from the database

    $anOrder = $this->orderRepository->find($anOrderId);


    // Update order status

    $anOrder->setStatus($anOrderStatus);


    // Update updatedAt field

    $anOrder->setUpdatedAt(new DateTimeImmutable());


    // Save the order to the database

    $this->orderRepository->save($anOrder);

    }

    }

    View Slide

  92. $changeOrderStatusService = new ChangeOrderStatusService(

    $orderRepository

    );


    $changeOrderStatusService->execute(1, Order::STATUS_ACCEPTED);

    View Slide

  93. An anemic domain model breaks encapsulation

    View Slide

  94. class ChangeOrderStatusService {

    private $orderRepository;


    public function __construct(OrderRepository $orderRepository) {

    $this->orderRepository = $orderRepository;

    }


    public function execute($anOrderId, $anOrderStatus) {

    // fetch an order from the database

    $anOrder = $this->orderRepository->find($anOrderId);


    // Update order status

    $anOrder->setStatus($anOrderStatus);


    // Update updatedAt field

    $anOrder->setUpdatedAt(new DateTimeImmutable());


    // Save the order to the database

    $this->orderRepository->save($anOrder);

    }

    }

    View Slide

  95. An anemic domain brings a false sense of
    code reuse

    View Slide

  96. class Order {

    // ...


    public function accept()

    {

    $this->setStatus(self::STATUS_ACCEPTED);

    $this->setUpdatedAt(new DateTimeImmutable());

    }

    }

    View Slide

  97. Repositories

    View Slide

  98. Mediates between the domain and data mapping layers
    using a collection-like interface for
    accessing domain objects

    View Slide

  99. For each type of object that needs global access, create
    an object that can provide the illusion of an in-memory
    collection of all objects of that type

    View Slide

  100. Repositories are mainly used to mediate with persistent
    stores.

    View Slide

  101. Collection-Oriented Repositories

    View Slide

  102. Good approach if the persistence mechanism

    tracks object changes.

    View Slide

  103. $order = new Order(

    OrderId::create(1),

    new Amount(100, Currency::EUR())

    );


    $orderRepository->add($order);

    $order->changeAmount(new Amount(200, Currency::EUR());


    assert($order == $orderRepository->orderOfId(OrderId::create(1)));

    View Slide

  104. $order = new Order(

    OrderId::create(1),

    new Amount(100, Currency::EUR())

    );


    $orderRepository->add($order);

    $order->changeAmount(new Amount(200, Currency::EUR());


    assert($order == $orderRepository->orderOfId(OrderId::create(1)));

    View Slide

  105. interface OrderRepository {

    function orderOfId(OrderId $anOrderId);

    function ordersOfUser(UserId $aUserId);

    function add(Order $anOrder);

    function remove(Order $anOrder);

    }

    View Slide

  106. use Doctrine\ORM\EntityRepository;


    class DoctrineOrderRepository extends EntityRepository implements OrderRepository {

    function orderOfId(OrderId $anOrderId) {

    return $this->find($anOrderId->id());

    }


    function ordersOfUser(UserId $aUserId) {

    $queryBuilder = $this->getEntityManager()->createQueryBuilder();

    $query = $queryBuilder

    ->select('o')

    ->from('Order', 'o')

    ->join('User', 'u')

    ->where('u.id = ?1')

    ->getQuery();

    ;
    $query->setParameters([1 => $aUserId->id()]);


    return $query->getResult();

    }


    function add(Order $anOrder) {

    $this->getEntityManager()->persist($anOrder);

    }


    function remove(Order $anOrder) {

    $this->getEntityManager()->remove($anOrder);

    }

    }

    View Slide

  107. Hides persistence details

    View Slide

  108. Persistence-Oriented Repositories

    View Slide

  109. Good approach if the persistence mechanism

    does not track object changes implicitly or explicitly.

    View Slide

  110. $order = new Order(

    OrderId::create(1),

    new Amount(100, Currency::EUR())

    );


    $orderRepository->save($order);


    // later on


    $order = $orderRepository->orderOfId(OrderId::create(1));

    $order->changeAmount(new Amount(200, Currency::EUR());


    $orderRepository->save($order);

    View Slide

  111. interface OrderRepository {

    function orderOfId(OrderId $anOrderId);

    function save(Order $anOrder);

    function remove(Order $anOrder);

    }

    View Slide

  112. class RedisOrderRepository implements OrderRepository {

    function orderOfId(OrderId $anOrderId) {

    return $this->unserialize(

    $this->redis->get(

    $this->computeKey($anOrderId)

    )

    );

    }


    function save(Order $anOrder) {

    $this->redis->set(

    $this->computeKey($anOrder->id()),

    $this->serialize($anOrder)

    );

    }


    function remove(Order $anOrder) {

    $this->redis->del(

    $this->computeKey($anOrder->id())

    );

    }

    }

    View Slide

  113. Repositories vs. DAOs

    View Slide

  114. How do we use them?

    View Slide

  115. class OrderApplicationService {

    private $orderRepository;


    public function __construct(OrderRepository $anOrderRepository) {

    $this->orderRepository = $anOrderRepository;

    }


    public function payOrder($orderId, $amount) {

    // ...

    }

    }

    View Slide

  116. $orderApplicationService = new OrderApplicationService(

    new DoctrineOrderRepository($entityManager)

    );


    $orderApplicationService->payOrder(/* ... */);

    View Slide

  117. Domain Events

    View Slide

  118. Something happened that domain experts care about.

    View Slide

  119. When the user signs up, a confirmation e-mail has
    to be sent.

    View Slide

  120. class UserSignedUp {

    // ...


    public function username() {

    return $this->username;

    }


    public function password() {

    return $this->password;

    }


    public function birthday() {

    return $this->birthday;

    }

    }

    View Slide

  121. Notify the customer vía e-mail
    when the order has been paid

    View Slide

  122. class OrderPaid {

    // ...


    public function orderId() {

    return $this->orderId;

    }


    public function amount() {

    return $this->amount;

    }

    }

    View Slide

  123. They are relevant occurrences expressed in

    past tense

    View Slide

  124. class Order {

    // ...


    public function pay() {

    // ...

    }

    }

    View Slide

  125. class Order {

    // ...


    public function pay() {

    // ...

    }

    }

    View Slide

  126. The order was paid

    View Slide

  127. OrderPaid

    View Slide

  128. Domain Events should include whatever is necessary
    to trigger the Event again.

    View Slide

  129. Domain Events are immutable.

    View Slide

  130. Use a lightweight Observer to publish them.

    View Slide

  131. class DomainEventPublisher {

    // ...


    private function __construct() {

    $this->subscribers = [];

    }


    public static function instance() {

    if (null === $this->instance) {

    $this->instance = new self();

    }


    return $this->instance;

    }

    }

    View Slide

  132. class DomainEventPublisher {

    // ...


    public function publish($aDomainEvent) {

    foreach ($this->subscribers as $aSubscriber) {

    if (get_class($aDomainEvent) === $aSubscriber->subscribedTo()) {

    $aSubscriber->handle($aDomainEvent);

    }

    }

    }

    }

    View Slide

  133. class DomainEventPublisher {

    // ...


    public function subscribe(DomainEventSubscriber $aSubscriber) {

    $this->subscribers[] = $aSubscriber;

    }

    }

    View Slide

  134. class Order {

    // ...


    public function pay() {

    // ...


    DomainEventPublisher::instance()->publish(

    new OrderPaid(

    $this->id(),

    $this->amount()

    )

    );

    }

    }

    View Slide

  135. class InvoiceForOrderPaidSubscriber implements DomainEventSubscriber {

    private $notifier;

    private $repository;


    public function handle($anEvent) {

    $anOrder = $this->repository->find($anEvent->orderId());


    $this->notifier->sendInvoiceFor($anOrder);

    }


    public function subscribedTo() {

    return OrderPaid::class;

    }

    }

    View Slide

  136. class OrderApplicationService {

    // ...


    public function payOrder($orderId) {

    DomainEventPublisher::instance()->subscribe(

    $this->invoiceForOrderPaidSubscriber

    );


    $anOrder = $this->repository->find($orderId);

    $anOrder->pay();

    }

    }

    View Slide

  137. How we do notify our business intelligence context

    about the new order created?

    View Slide

  138. If we have a gamification context to reward our customers

    for his purchases, how do we notify them?

    View Slide

  139. Domain Events are a domain-wide concept.

    View Slide

  140. Domain Events are an integration mechanism.

    View Slide

  141. Sum up

    View Slide

  142. Tactical design patterns are just a tool.

    View Slide

  143. Tactical design patterns enable us to express

    the model in an elegant object-oriented

    way.

    View Slide

  144. They are not mandatory to practice DDD.

    View Slide

  145. ¡Thanks!
    @theUniC

    View Slide