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

Unbreakable Domain Models - DPC13

Unbreakable Domain Models - DPC13

DataMappers like Doctrine2 help us a lot to persist data. Yet many projects are still struggling with tough questions:
- Where to put business logic?
- How to prevent our code from abuse?
- Where to put queries, and how test them?

It’s time to look beyond the old Gang of Four design patterns. There are Value Objects, Entities and Aggregates at the core; Repositories for persistence; Specifications to accurately describe object selections; Encapsulated Operations to protect invariants; and Domain Services and Double Dispatch when we need to group behavior safely. These patterns help us evolve from structural data models, to rich behavioral models. They capture not just state and relationships, but true meaning. These patterns protect our models from being used incorrectly, and allow us to test the essence of our applications.

The presentation is a fast paced introduction to the patterns that will make your Domain Model expressive, unbreakable, and beautiful.

Update: Replaced the slides with the slightly better slides from FrOSCon 2013

Mathias Verraes

June 07, 2013
Tweet

More Decks by Mathias Verraes

Other Decks in Programming

Transcript

  1. Unbreakable
    Domain Models
    I’m Mathias Verraes.
    I cure complex
    legacy projects.
    @mathiasverraes
    http://verraes.net

    View full-size slide

  2. Domain
    Problem Space
    Domain Model
    Solution Space

    View full-size slide

  3. (Data Model
    The model’s state)

    View full-size slide

  4. Protect your invariants

    View full-size slide

  5. The domain expert says
    “A customer must
    always have an
    email address.”
    * Could be different for your domain
    ** All examples are simplified

    View full-size slide

  6. class CustomerTest extends PHPUnit_Framework_TestCase
    {
    /** @test */
    public function should_always_have_an_email()
    {
    $customer = new Customer();
    assertThat(
    $customer->getEmail(),
    equalTo('[email protected]')
    );
    }
    }
    Test fails

    View full-size slide

  7. class CustomerTest extends PHPUnit_Framework_TestCase
    {
    /** @test */
    public function should_always_have_an_email()
    {
    $customer = new Customer();
    $customer->setEmail('[email protected]');
    assertThat(
    $customer->getEmail(),
    equalTo('[email protected]')
    );
    }
    }
    Test passes

    View full-size slide

  8. class CustomerTest extends PHPUnit_Framework_TestCase
    {
    /** @test */
    public function should_always_have_an_email()
    {
    $customer = new Customer();
    assertThat(
    $customer->getEmail(),
    equalTo(‘[email protected]')
    );
    $customer->setEmail(‘[email protected]’);
    }
    }
    Test fails

    View full-size slide

  9. class Customer
    {
    private $email;
    public function __construct($email)
    {
    $this->email = $email;
    }
    public function getEmail()
    {
    return $this->email;
    }
    }
    Test passes

    View full-size slide

  10. class CustomerTest extends PHPUnit_Framework_TestCase
    {
    /** @test */
    public function should_always_have_an_email()
    {
    $customer = new Customer(‘[email protected]’);
    assertThat(
    $customer->getEmail(),
    equalTo(‘[email protected]')
    );
    }
    }
    Test passes

    View full-size slide

  11. Use objects as
    consistency boundaries

    View full-size slide

  12. The domain expert meant
    “A customer must
    always have a valid
    email address.”

    View full-size slide

  13. $customerValidator = new CustomerValidator;
    if($customerValidator->isValid($customer)){
    // ...
    }

    View full-size slide

  14. class CustomerTest extends PHPUnit_Framework_TestCase
    {
    /** @test */
    public function should_always_have_a_valid_email()
    {
    $this->setExpectedException(
    '\InvalidArgumentException'
    );
    new Customer('malformed@email');
    }
    }
    Test fails

    View full-size slide

  15. class Customer
    {
    public function __construct($email)
    {
    if( /* ugly regex here */) {
    throw new \InvalidArgumentException();
    }
    $this->email = $email;
    }
    }
    Test passes

    View full-size slide

  16. Violates
    Single Responsibility
    Principle

    View full-size slide

  17. class Email
    {
    private $email;
    public function __construct($email)
    {
    if( /* ugly regex here */) {
    throw new \InvalidArgumentException();
    }
    $this->email = $email;
    }
    public function __toString()
    {
    return $this->email;
    }
    }
    Test passes

    View full-size slide

  18. class Customer
    {
    /** @var Email */
    private $email;
    public function __construct(Email $email)
    {
    $this->email = $email;
    }
    }
    Test passes

    View full-size slide

  19. class CustomerTest extends PHPUnit_Framework_TestCase
    {
    /** @test */
    public function should_always_have_a_valid_email()
    {
    $this->setExpectedException(
    ‘\InvalidArgumentException’
    );
    new Customer(new Email(‘malformed@email’));
    }
    }
    Test passes

    View full-size slide

  20. Encapsulate
    state and behavior
    with Value Objects

    View full-size slide

  21. The domain expert says
    “A customer
    orders products
    and pays for them.”

    View full-size slide

  22. $order = new Order;
    $order->setCustomer($customer);
    $order->setProducts($products);
    $order->setStatus(Order::UNPAID);
    // ...
    $order->setPaidAmount(500);
    $order->setPaidCurrency(‘EUR’);
    $order->setStatus(Order::PAID);

    View full-size slide

  23. $order = new Order;
    $order->setCustomer($customer);
    $order->setProducts($products);
    $order->setStatus(
    new PaymentStatus(PaymentStatus::UNPAID)
    );
    $order->setPaidAmount(500);
    $order->setPaidCurrency(‘EUR’);
    $order->setStatus(
    new PaymentStatus(PaymentStatus::PAID)
    );

    View full-size slide

  24. $order = new Order;
    $order->setCustomer($customer);
    $order->setProducts($products);
    $order->setStatus(
    new PaymentStatus(PaymentStatus::UNPAID)
    );
    $order->setPaidMonetary(
    new Money(500, new Currency(‘EUR’))
    );
    $order->setStatus(
    new PaymentStatus(PaymentStatus::PAID)
    );

    View full-size slide

  25. $order = new Order($customer, $products);
    // set PaymentStatus in Order::__construct()
    $order->setPaidMonetary(
    new Money(500, new Currency(‘EUR’))
    );
    $order->setStatus(
    new PaymentStatus(PaymentStatus::PAID)
    );

    View full-size slide

  26. $order = new Order($customer, $products);
    $order->pay(
    new Money(500, new Currency(‘EUR’))
    );
    // set PaymentStatus in Order#pay()

    View full-size slide

  27. Encapsulate operations

    View full-size slide

  28. $order = $customer->order($products);
    $customer->pay(
    $order,
    new Money(500, new Currency(‘EUR’))
    );

    View full-size slide

  29. The domain expert says
    “Premium customers
    get special offers.”

    View full-size slide

  30. if($customer->isPremium()) {
    // send special offer
    }

    View full-size slide

  31. The domain expert says
    “Order 3 times
    to become a
    premium customer.”

    View full-size slide

  32. interface CustomerSpecification
    {
    /** @return bool */
    public function isSatisfiedBy(Customer $customer);
    }

    View full-size slide

  33. class CustomerIsPremium implements CustomerSpecification
    {
    private $orderRepository;
    public function __construct(
    OrderRepository $orderRepository
    ) {...}
    /** @return bool */
    public function isSatisfiedBy(Customer $customer)
    {
    $count = $this->orderRepository->countFor($customer);
    return $count >= 3;
    }
    }
    $customerIsPremium = new CustomerIsPremium($orderRepository)
    if($customerIsPremium->isSatisfiedBy($customer)) {
    // send special offer
    }

    View full-size slide

  34. $customerIsPremium = new CustomerIsPremium;
    $aCustomerWith2Orders = ...
    $aCustomerWith3Orders = ...
    assertFalse(
    $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders)
    );
    assertTrue(
    $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders)
    );

    View full-size slide

  35. The domain expert says
    “Different rules apply
    for different tenants.”

    View full-size slide

  36. interface CustomerIsPremium
    extends CustomerSpecification
    class CustomerWith3OrdersIsPremium
    implements CustomerIsPremium
    class CustomerWith500EuroTotalIsPremium
    implements CustomerIsPremium
    class CustomerWhoBoughtLuxuryProductsIsPremium
    implements CustomerIsPremium
    ...

    View full-size slide

  37. class SpecialOfferSender
    {
    private $customerIsPremium;
    public function __construct(
    CustomerIsPremium $customerIsPremium) {...}
    public function sendOffersTo(Customer $customer)
    {
    if($this->customerIsPremium->isSatisfiedBy(
    $customer
    ))
    {
    // send offers...
    }
    }
    }

    View full-size slide


  38. class="CustomerWith500EuroTotalIsPremium">

    class="CustomerWith3OrdersIsPremium">

    id=”special.offer.sender”
    class=”SpecialOfferSender”>


    View full-size slide

  39. Use specifications to
    encapsulate rules
    about object selection

    View full-size slide

  40. The domain expert says
    “Get a list of
    all premium
    customers.”

    View full-size slide

  41. interface CustomerRepository
    {
    public function add(Customer $customer);
    public function remove(Customer $customer);
    /** @return Customer */
    public function find(CustomerId $customerId);
    /** @return Customer[] */
    public function findAll();
    /** @return Customer[] */
    public function findRegisteredIn(Year $year);
    }

    View full-size slide

  42. interface CustomerRepository
    {
    /** @return Customer[] */
    public function findSatisfying(
    CustomerSpecification $customerSpecification
    );
    }
    // generalized:
    $objects = $repository->findSatisfying($specification);

    View full-size slide

  43. class DbCustomerRepository implements CustomerRepository
    {
    /** @return Customer[] */
    public function findSatisfying(
    CustomerSpecification $customerSpecification)
    {
    // filter Customers (see next slide)
    }
    }

    View full-size slide

  44. // class DbCustomerRepository
    public function findSatisfying($specification)
    {
    $foundCustomers = array();
    foreach($this->findAll() as $customer) {
    if($specification->isSatisfiedBy($customer)) {
    $foundCustomers[] = $customer;
    }
    }
    return $foundCustomers;
    }

    View full-size slide

  45. class CustomerWith3OrdersIsPremium
    implements CustomerSpecification
    {
    public function asSql() {
    return ‘SELECT * FROM Customer...’;
    }
    }
    // class DbCustomerRepository
    public function findSatisfying($specification)
    {
    return $this->db->query($specification->asSql());
    }

    View full-size slide

  46. Use double dispatch
    to preserve encapsulation

    View full-size slide

  47. $expectedCustomers = // filtered using isSatisfiedBy
    $actualCustomers =
    $repository->findSatisfying($specification);
    assertThat($expectedCustomers, equalTo($actualCustomers));

    View full-size slide

  48. Test by comparing
    different representations

    View full-size slide

  49. Protect your invariants
    Objects as
    consistency boundaries
    Encapsulate
    state and behavior

    View full-size slide

  50. More? google for:
    Eric Evans
    Vaugh Vernon
    Martin Fowler
    Greg Young
    Udi Dahan
    Sandro Marcuso
    Yves Reynhout
    Szymon Pobiega
    Alberto Brandolini
    ...

    View full-size slide

  51. Thanks! Questions?
    @mathiasverraes
    http://verraes.net
    https://joind.in/8438

    View full-size slide