$30 off During Our Annual Pro Sale. View Details »

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. Agenda Tactical Design Patterns I Tactical Design Patterns II Value

    Objects Entities Domain Services Repositories Domain Events Final Thoughts
  2. “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”
  3. “A small simple object, like money or a date range,

    whose equality isn’t based on identity”
  4. class Currency {
 private $isoCode;
 
 public function __construct($isoCode) {


    $this->isoCode = $isoCode;
 }
 
 public function isoCode() {
 return $this->isoCode;
 }
 }
  5. 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;
 }
 }
  6. class Money {
 // ...
 
 public function increaseBy(Money $aQuantity)

    {
 $this->assertMoneyHasSameCurrency($aQuantity);
 
 return new self(
 $this->amount + $aQuantity->amount,
 $this->currency
 );
 }
 }
  7. class Money {
 // ...
 
 public function increaseBy(Money $aQuantity)

    {
 $this->assertMoneyHasSameCurrency($aQuantity);
 
 return new self(
 $this->amount + $aQuantity->amount,
 $this->currency
 );
 }
 }
  8. class Money {
 // ...
 
 public function increaseBy(Money $aQuantity)

    {
 $this->assertMoneyHasSameCurrency($aQuantity);
 
 return new self(
 $this->amount + $aQuantity->amount,
 $this->currency
 );
 }
 }
  9. 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;
 }
 }
  10. $amount1 = new Money(100, Currency::USD());
 $amount2 = new Money(100, Currency::EUR());


    
 false === assert(
 ($amount1->currency()->isoCode() === $amount2->currency()->isoCode())
 && ($amount1->amount() === $amount2->amount())
 );
  11. class Currency {
 // ...
 
 public function equalsTo(self $anotherCurrency)

    {
 return $this->isoCode === $anotherCurrency->isoCode;
 }
 }
  12. class Money {
 // ...
 
 public function equalsTo(self $anotherMoney)

    {
 return $this->currency->equalsTo($anotherMoney->currency)
 && $this->amount === $anotherMoney->amount;
 }
 }
  13. 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;
 }
 }
  14. 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;
 }
 }
  15. 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;
 }
 }
  16. CREATE TABLE `orders` (
 `id` INT(11) NOT NULL AUTO_INCREMENT,
 `amount`

    DECIMAL(10, 5) NOT NULL,
 PRIMARY KEY (`id`)
 ) ENGINE=InnoDB;
  17. class ISBN {
 private $isbn;
 
 public function __construct($anIsbn) {


    $this->isbn = $anIsbn;
 }
 
 public function isbn() {
 return $this->isbn;
 }
 }
  18. class Book {
 private $isbn;
 private $title;
 
 public function

    __construct(ISBN $anIsbn, $aTitle) {
 $this->isbn = $anIsbn;
 $this->title = $aTitle;
 }
 }
  19. 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;
 }
 }
  20. class EmailAddress {
 // ...
 
 private function assertNotNull($anAddress) {


    if (null === $anAddress) {
 throw new InvalidArgumentException();
 }
 }
 }
  21. class EmailAddress {
 // ...
 
 private function assertLengthGreaterThan($anAddress, $min)

    {
 if (strlen($anAddress) <= $min) {
 throw new InvalidArgumentException();
 }
 }
 }
  22. class EmailAddress {
 // ...
 
 private function assertLengthLowerThan($anAddress, $max)

    {
 if (strlen($anAddress) >= $max) {
 throw new InvalidArgumentException();
 }
 }
 }
  23. class EmailAddress {
 // ...
 
 private function assertIsValidEmailAddress($anAddress) {


    if (false === filter_var($anAddress, FILTER_VALIDATE_EMAIL)) {
 throw new InvalidArgumentException();
 }
 }
 }
  24. 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;
 }
 
 // ...
 }
  25. 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);
 }
  26. 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!');
 }
 }
 
 }
  27. // 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();
  28. Active Record couples the design of the database to the

    design of the object-oriented system
  29. Most of the implementations force the use, through inheritance, of

    some sort of constructions to impose their conventions.
  30. 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;
 }
 }
  31. 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;
 }
 }
  32. abstract class IdentifiableDomainObject {
 private $id;
 
 protected function id()

    {
 return $this->id;
 }
 
 protected function setId($anId) {
 $this->id = $anId;
 }
 }
  33. class Order extends IdentifiableDomainObject {
 private $orderId;
 
 public function

    orderId() {
 if (null === $this->orderId) {
 $this->orderId = new OrderId($this->id());
 }
 
 return $this->orderId;
 }
 }
  34. 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;
  35. 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;}
 }
  36. // 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);
  37. 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);
 }
 }
  38. 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);
 }
 }
  39. class Order {
 // ...
 
 public function accept()
 {


    $this->setStatus(self::STATUS_ACCEPTED);
 $this->setUpdatedAt(new DateTimeImmutable());
 }
 }
  40. Mediates between the domain and data mapping layers using a

    collection-like interface for accessing domain objects
  41. 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
  42. $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)));
  43. $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)));
  44. 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);
 }
 }
  45. $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);
  46. 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())
 );
 }
 }
  47. class OrderApplicationService {
 private $orderRepository;
 
 public function __construct(OrderRepository $anOrderRepository)

    {
 $this->orderRepository = $anOrderRepository;
 }
 
 public function payOrder($orderId, $amount) {
 // ...
 }
 }
  48. class UserSignedUp {
 // ...
 
 public function username() {


    return $this->username;
 }
 
 public function password() {
 return $this->password;
 }
 
 public function birthday() {
 return $this->birthday;
 }
 }
  49. class OrderPaid {
 // ...
 
 public function orderId() {


    return $this->orderId;
 }
 
 public function amount() {
 return $this->amount;
 }
 }
  50. class DomainEventPublisher {
 // ...
 
 private function __construct() {


    $this->subscribers = [];
 }
 
 public static function instance() {
 if (null === $this->instance) {
 $this->instance = new self();
 }
 
 return $this->instance;
 }
 }
  51. class DomainEventPublisher {
 // ...
 
 public function publish($aDomainEvent) {


    foreach ($this->subscribers as $aSubscriber) {
 if (get_class($aDomainEvent) === $aSubscriber->subscribedTo()) {
 $aSubscriber->handle($aDomainEvent);
 }
 }
 }
 }
  52. class Order {
 // ...
 
 public function pay() {


    // ...
 
 DomainEventPublisher::instance()->publish(
 new OrderPaid(
 $this->id(),
 $this->amount()
 )
 );
 }
 }
  53. 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;
 }
 }
  54. class OrderApplicationService {
 // ...
 
 public function payOrder($orderId) {


    DomainEventPublisher::instance()->subscribe(
 $this->invoiceForOrderPaidSubscriber
 );
 
 $anOrder = $this->repository->find($orderId);
 $anOrder->pay();
 }
 }
  55. If we have a gamification context to reward our customers


    for his purchases, how do we notify them?