Unbreakable Domain Models - FrOSCon 2013

1,037 views

Published on

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.

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

Published in: Technology, Business
0 Comments
2 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,037
On SlideShare
0
From Embeds
0
Number of Embeds
50
Actions
Shares
0
Downloads
9
Comments
0
Likes
2
Embeds 0
No embeds

No notes for slide

Unbreakable Domain Models - FrOSCon 2013

  1. 1. Unbreakable Domain Models Mathias Verraes FrOSCon Sankt-Augustin, DE August 24, 2013 @mathiasverraes http://verraes.net
  2. 2. I'm an independent consultant. I build enterprise web applications.
  3. 3. I help teams escape from survival mode.
  4. 4. Cofounder of the Belgian Domain-Driven Design community http://domaindriven.be @DDDBE Modellathon on September 3rd, 2013 Ghent
  5. 5. Domain Problem Space Domain Model Solution Space
  6. 6. (Data Model The model’s state)
  7. 7. Protect your invariants
  8. 8. The domain expert says “A customer must always have an email address.” * Could be different for your domain ** All examples are simplified
  9. 9. 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
  10. 10. 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
  11. 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') ); $customer->setEmail(‘jim@example.com’); } } Test fails
  12. 12. class Customer { private $email; public function __construct($email) { $this->email = $email; } public function getEmail() { return $this->email; } } Test passes
  13. 13. 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
  14. 14. Use objects as consistency boundaries
  15. 15. class ProspectiveCustomer { //... /** @return PayingCustomer */ public function convertToPayingCustomer(){ } } class PayingCustomer { ... }
  16. 16. Make the implicit explicit
  17. 17. The domain expert meant “A customer must always have a valid email address.”
  18. 18. $customerValidator = new CustomerValidator; if($customerValidator->isValid($customer)){ // ... }
  19. 19. class CustomerTest extends PHPUnit_Framework_TestCase { /** @test */ public function should_always_have_a_valid_email() { $this->setExpectedException( 'InvalidArgumentException' ); new Customer('malformed@email'); } } Test fails
  20. 20. class Customer { public function __construct($email) { if( /* ugly regex here */) { throw new InvalidArgumentException(); } $this->email = $email; } } Test passes
  21. 21. Violates Single Responsibility Principle
  22. 22. 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
  23. 23. class Customer { /** @var Email */ private $email; public function __construct(Email $email) { $this->email = $email; } } Test passes
  24. 24. 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
  25. 25. Encapsulate state and behavior with Value Objects
  26. 26. The domain expert says “A customer orders products and pays for them.”
  27. 27. $order = new Order; $order->setCustomer($customer); $order->setProducts($products); $order->setStatus(Order::UNPAID); // ... $order->setPaidAmount(500); $order->setPaidCurrency(‘EUR’); $order->setStatus(Order::PAID);
  28. 28. $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) );
  29. 29. $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) );
  30. 30. $order = new Order($customer, $products); // set PaymentStatus in Order::__construct() $order->setPaidMonetary( new Money(500, new Currency(‘EUR’)) ); $order->setStatus( new PaymentStatus(PaymentStatus::PAID) );
  31. 31. $order = new Order($customer, $products); $order->pay( new Money(500, new Currency(‘EUR’)) ); // set PaymentStatus in Order#pay()
  32. 32. Encapsulate operations
  33. 33. $order = $customer->order($products); $customer->pay( $order, new Money(500, new Currency(‘EUR’)) );
  34. 34. The domain expert says “Premium customers get special offers.”
  35. 35. if($customer->isPremium()) { // send special offer }
  36. 36. The domain expert says “Order 3 times to become a premium customer.”
  37. 37. interface CustomerSpecification { /** @return bool */ public function isSatisfiedBy(Customer $customer); }
  38. 38. 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 }
  39. 39. $customerIsPremium = new CustomerIsPremium; $aCustomerWith2Orders = ... $aCustomerWith3Orders = ... assertFalse( $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders) ); assertTrue( $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders) );
  40. 40. The domain expert says “Different rules apply for different tenants.”
  41. 41. interface CustomerIsPremium extends CustomerSpecification class CustomerWith3OrdersIsPremium implements CustomerIsPremium class CustomerWith500EuroTotalIsPremium implements CustomerIsPremium class CustomerWhoBoughtLuxuryProductsIsPremium implements CustomerIsPremium ...
  42. 42. class SpecialOfferSender { private $customerIsPremium; public function __construct( CustomerIsPremium $customerIsPremium) {...} public function sendOffersTo(Customer $customer) { if($this->customerIsPremium->isSatisfiedBy( $customer )) { // send offers... } } }
  43. 43. <!-- 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>
  44. 44. Use specifications to encapsulate rules about object selection
  45. 45. The domain expert says “Get a list of all premium customers.”
  46. 46. 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); }
  47. 47. interface CustomerRepository { /** @return Customer[] */ public function findSatisfying( CustomerSpecification $customerSpecification ); } // generalized: $objects = $repository->findSatisfying($specification);
  48. 48. class DbCustomerRepository implements CustomerRepository { /** @return Customer[] */ public function findSatisfying( CustomerSpecification $customerSpecification) { // filter Customers (see next slide) } }
  49. 49. // class DbCustomerRepository public function findSatisfying($specification) { $foundCustomers = array(); foreach($this->findAll() as $customer) { if($specification->isSatisfiedBy($customer)) { $foundCustomers[] = $customer; } } return $foundCustomers; }
  50. 50. class CustomerWith3OrdersIsPremium implements CustomerSpecification { public function asSql() { return ‘SELECT * FROM Customer...’; } } // class DbCustomerRepository public function findSatisfying($specification) { return $this->db->query($specification->asSql()); }
  51. 51. Use double dispatch to preserve encapsulation
  52. 52. $expectedCustomers = // filtered using isSatisfiedBy $actualCustomers = $repository->findSatisfying($specification); assertThat($expectedCustomers, equalTo($actualCustomers));
  53. 53. Test by comparing different representations
  54. 54. Protect your invariants Objects as consistency boundaries Encapsulate state and behavior
  55. 55. More? google for: Eric Evans Vaugh Vernon Martin Fowler Greg Young Udi Dahan Sandro Marcuso Yves Reynhout Szymon Pobiega Alberto Brandolini ...
  56. 56. Thanks! Questions? @mathiasverraes http://verraes.net https://joind.in/9020

×