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

Unbreakable Domain Models - PHPUK 2014 London

Unbreakable Domain Models - PHPUK 2014 London

Data Mappers (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 protect our code from abuse?
Where to put queries, and how test them?

Let’s look beyond the old Gang of Four design patterns, and take some clues from tactical Domain Driven Design. At the heart of our models, we can use Value Objects and Entities, with tightly defined consistency boundaries. Repositories abstract away the persistence. Encapsulated Operations helps us to protect invariants. And if we need to manage a lot of complexity, the Specification pattern helps us express business rules in the language of the business. These patterns help us evolve from structural data models, to rich behavioral models. They capture not just state and relationships, but true meaning. The presentation is a fast paced introduction to some patterns and ideas that will make your Domain Model expressive, unbreakable, and beautiful.

330627d5f564b710721236077903ed60?s=128

Mathias Verraes

February 21, 2014
Tweet

Transcript

  1. Unbreakable Domain Models @mathiasverraes

  2. A Map of the World

  3. London Paris Amsterdam Kortrijk, Belgium 3h train rides

  4. All models are wrong, but some are useful.

  5. I'm an independent consultant. I help teams build enterprise web

    applications. I’m Mathias Verraes
  6. Blog verraes.net ! Podcast with @everzet elephantintheroom.io ! DDD in

    PHP bit.ly/dddinphp
  7. Domain Problem Space Domain Model Solution Space

  8. Data Model ~= Structural Model ~= State ! Domain Model

    ~= Behavioral Model !
  9. Protect your invariants

  10. The domain expert says “A customer must always have an

     email address.” * Could be different for your domain ** All examples are simplified
  11. class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function

    should_always_have_an_email()! {! ! $customer = new Customer();! ! assertThat(! $customer->getEmail(),! equalTo('jim@example.com') ! );! ! }! } Test fails
  12. class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function

    should_always_have_an_email()! {! ! $customer = new Customer();! $customer->setEmail('jim@example.com');! assertThat(! $customer->getEmail(),! equalTo('jim@example.com') ! );! }! } Test passes
  13. class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function

    should_always_have_an_email()! {! ! $customer = new Customer();! assertThat(! $customer->getEmail(),! equalTo(‘jim@example.com') ! );! $customer->setEmail(‘jim@example.com’);! ! }! } Test fails
  14. final class Customer! {! private $email;! ! public function __construct($email)!

    {! $this->email = $email;! }! ! public function getEmail()! {! return $this->email;! }! }
  15. class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function

    should_always_have_an_email()! {! ! $customer = new Customer(‘jim@example.com’);! ! assertThat(! $customer->getEmail(),! equalTo(‘jim@example.com') ! );! }! } Test passes
  16. Use objects as consistency boundaries

  17. final class ProspectiveCustomer ! {! public function __construct()! {! //

    no email! }! }! ! final class PayingCustomer ! { ! public function __construct($email)! {! $this->email = $email;! }! }
  18. Make the implicit explicit

  19. final class ProspectiveCustomer ! {! /** @return PayingCustomer */! public

    function convertToPayingCustomer($email)! { ! //...! }! }! ! final class PayingCustomer ! { ! //...! }
  20. The domain expert meant “A customer must always have a

    valid  email address.”
  21. $customerValidator = new CustomerValidator;! if($customerValidator->isValid($customer)){! // ...! }

  22. class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function

    should_always_have_a_valid_email()! {! ! $this->setExpectedException(! '\InvalidArgumentException'! );! ! new Customer('malformed@email');! ! }! } Test fails
  23. final class Customer ! {! public function __construct($email)! {! if(

    /* boring validation stuff */) {! throw new \InvalidArgumentException();! }! $this->email = $email;! }! } Test passes
  24. Violates Single Responsibility Principle

  25. final class Email! {! private $email;! ! public function __construct($email)!

    {! if( /* boring validation stuff */) {! throw new \InvalidArgumentException();! }! $this->email = $email;! }! ! public function __toString() ! {! return $this->email;! } ! } Test passes
  26. final class Customer! {! /** @var Email */! private $email;!

    ! public function __construct(Email $email)! {! $this->email = $email;! }! } Test passes
  27. 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
  28. Entity ! Equality by Identity Lifecycle Mutable Value Object Equality

    by Value ! Immutable
  29. Encapsulate  state and behavior with Value Objects

  30. The domain expert says “A customer  orders products and

    pays for them.”
  31. $order = new Order;! $order->setCustomer($customer);! $order->setProducts($products);! $order->setStatus(Order::UNPAID);! ! ! //

    ...! ! ! $order->setPaidAmount(500);! $order->setPaidCurrency(‘EUR’);! ! $order->setStatus(Order::PAID);! !
  32. $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)! );
  33. $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)! );
  34. $order = new Order($customer, $products);! // set PaymentStatus in Order::__construct()!

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

    ! ! ! $order->pay(! new Money(500, new Currency(‘EUR’))! );! // set PaymentStatus in Order#pay()! !
  36. Encapsulate operations

  37. $order = $customer->order($products);! ! ! ! ! ! ! !

    ! $customer->payFor(! $order,! new Money(500, new Currency(‘EUR’))! );! !
  38. The domain expert says “Premium customers  get special offers.”

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

  40. The domain expert says “Order 3 times  to become

    a  premium customer.”
  41. interface CustomerSpecification ! {! /** @return bool */! public function

    isSatisfiedBy(Customer $customer); ! }
  42. 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! }!
  43. $customerIsPremium = new CustomerIsPremium;! ! $aCustomerWith2Orders = ...! $aCustomerWith3Orders =

    ...! ! assertFalse(! $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders)! );! ! assertTrue(! $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders)! );! ! !
  44. The domain expert says “Different rules apply for different tenants.”

  45. interface CustomerIsPremium ! extends CustomerSpecification! ! final class CustomerWith3OrdersIsPremium !

    implements CustomerIsPremium! ! final class CustomerWith500EuroTotalIsPremium! implements CustomerIsPremium! ! final class CustomerWhoBoughtLuxuryProductsIsPremium! implements CustomerIsPremium! ! ...!
  46. final class SpecialOfferSender! {! private $customerIsPremium;! ! ! public function

    __construct(! CustomerIsPremium $customerIsPremium) {...}! ! ! public function sendOffersTo(Customer $customer) ! {! if($this->customerIsPremium->isSatisfiedBy(! $customer! )) ! {! // send offers...! }! }! }!
  47. ! <!-- if you load services_amazon.xml: -->! <service id="customer.is.premium"! class="CustomerWith500EuroTotalIsPremium">

    ! ! <!-- if you load services_ebay.xml: -->! <service id="customer.is.premium"! class="CustomerWith3OrdersIsPremium"> ! ! ! <!-- elsewhere -->! <service ! id=”special.offer.sender”! class=”SpecialOfferSender”>! <argument type=”service” id=”customer.is.premium”/>! </service>
  48. Use specifications to encapsulate rules  about object selection

  49. The domain expert says “Get a list of  all

    premium  customers.”
  50. 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);! }!
  51. Use repositories to  create the illusion of  in-memory

    collections
  52. interface CustomerRepository! {! ! /** @return Customer[] */! public function

    findSatisfying(! CustomerSpecification $customerSpecification! );! ! }! ! ! // generalized:! $objects = $repository->findSatisfying($specification);!
  53. class DbCustomerRepository implements CustomerRepository! {! /** @return Customer[] */! public

    function findSatisfying(! CustomerSpecification $specification) ! {! ! return array_filter(! $this->findAll(),! function(Customer $customer) use($specification) {! return $specification->isSatisfiedBy($customer);! } ! );! ! }! }!
  54. final class CustomerWith3OrdersIsPremium! implements CustomerSpecification! {! public function asSql() {!

    return ‘SELECT * FROM Customer...’;! }! }! ! ! // class DbCustomerRepository ! public function findSatisfying($specification) ! {! return $this->db->query($specification->asSql()); ! }
  55. Use double dispatch to preserve encapsulation

  56. $expectedCustomers = array_filter(! $repository->findAll(),! // filter…! );! ! $actualCustomers =

    ! $repository->findSatisfying($specification);! ! assertThat($expectedCustomers, equalTo($actualCustomers));
  57. Test by comparing  different representations of the same rule

  58. Protect your invariants ! Objects as  consistency boundaries !

    Encapsulate  state and behavior
  59. Thanks! Questions? ! Blog, Slides, other talks: verraes.net @mathiasverraes I

    ♥ Feedback joind.in/10690