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

Unbreakable Domain Models - FrOSCon 2013

Unbreakable Domain Models - FrOSCon 2013

More at http://verraes.net/ or http://twitter.com/mathiasverraes

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

August 24, 2013
Tweet

More Decks by Mathias Verraes

Other Decks in Technology

Transcript

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

    email address.” * Could be different for your domain ** All examples are simplified
  2. 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
  3. 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
  4. 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
  5. class Customer { private $email; public function __construct($email) { $this->email

    = $email; } public function getEmail() { return $this->email; } } Test passes
  6. 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
  7. class ProspectiveCustomer { //... /** @return PayingCustomer */ public function

    convertToPayingCustomer(){ } } class PayingCustomer { ... }
  8. class CustomerTest extends PHPUnit_Framework_TestCase { /** @test */ public function

    should_always_have_a_valid_email() { $this->setExpectedException( '\InvalidArgumentException' ); new Customer('malformed@email'); } } Test fails
  9. class Customer { public function __construct($email) { if( /* ugly

    regex here */) { throw new \InvalidArgumentException(); } $this->email = $email; } } Test passes
  10. 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
  11. class Customer { /** @var Email */ private $email; public

    function __construct(Email $email) { $this->email = $email; } } Test passes
  12. 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
  13. $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) );
  14. $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) );
  15. $order = new Order($customer, $products); // set PaymentStatus in Order::__construct()

    $order->setPaidMonetary( new Money(500, new Currency(‘EUR’)) ); $order->setStatus( new PaymentStatus(PaymentStatus::PAID) );
  16. 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 }
  17. $customerIsPremium = new CustomerIsPremium; $aCustomerWith2Orders = ... $aCustomerWith3Orders = ...

    assertFalse( $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders) ); assertTrue( $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders) );
  18. interface CustomerIsPremium extends CustomerSpecification class CustomerWith3OrdersIsPremium implements CustomerIsPremium class CustomerWith500EuroTotalIsPremium

    implements CustomerIsPremium class CustomerWhoBoughtLuxuryProductsIsPremium implements CustomerIsPremium ...
  19. class SpecialOfferSender { private $customerIsPremium; public function __construct( CustomerIsPremium $customerIsPremium)

    {...} public function sendOffersTo(Customer $customer) { if($this->customerIsPremium->isSatisfiedBy( $customer )) { // send offers... } } }
  20. <!-- 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>
  21. 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); }
  22. interface CustomerRepository { /** @return Customer[] */ public function findSatisfying(

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

    function findSatisfying( CustomerSpecification $customerSpecification) { // filter Customers (see next slide) } }
  24. // class DbCustomerRepository public function findSatisfying($specification) { $foundCustomers = array();

    foreach($this->findAll() as $customer) { if($specification->isSatisfiedBy($customer)) { $foundCustomers[] = $customer; } } return $foundCustomers; }
  25. class CustomerWith3OrdersIsPremium implements CustomerSpecification { public function asSql() { return

    ‘SELECT * FROM Customer...’; } } // class DbCustomerRepository public function findSatisfying($specification) { return $this->db->query($specification->asSql()); }
  26. More? google for: Eric Evans Vaugh Vernon Martin Fowler Greg

    Young Udi Dahan Sandro Marcuso Yves Reynhout Szymon Pobiega Alberto Brandolini ...