Unbreakable
Domain Models
@mathiasverraes
A Map of the World
London
3h train rides

Kortrijk, Belgium

Paris

Amsterdam
All models are wrong,
but some are useful.
I’m Mathias Verraes
I'm an independent
consultant.
I help teams build
enterprise web
applications.
Blog
verraes.net
!

Podcast with @everzet
elephantintheroom.io
!

DDD in PHP
bit.ly/dddinphp
Domain
Problem Space
Domain Model
Solution Space
Data Model ~= Structural Model ~= State
!

Domain Model ~= Behavioral Model
!
Protect your invariants
The domain expert says

“A customer must
always have an
email address.”
* Could be different for your domain
** All exampl...
Test fails

class CustomerTest extends PHPUnit_Framework_TestCase!
{!
/** @test */!
public function should_always_have_an_...
Test passes

class CustomerTest extends PHPUnit_Framework_TestCase!
{!
/** @test */!
public function should_always_have_an...
Test fails

class CustomerTest extends PHPUnit_Framework_TestCase!
{!
/** @test */!
public function should_always_have_an_...
final class Customer!
{!
private $email;!
!

public function __construct($email)!
{!
$this->email = $email;!
}!
!

public ...
Test passes

class CustomerTest extends PHPUnit_Framework_TestCase!
{!
/** @test */!
public function should_always_have_an...
Use objects as
consistency boundaries
final class ProspectiveCustomer !
{!
public function __construct()!
{!
// no email!
}!
}!
!

final class PayingCustomer !
...
Make the implicit
explicit
final class ProspectiveCustomer !
{!
/** @return PayingCustomer */!
public function convertToPayingCustomer($email)!
{ !
/...
The domain expert meant

“A customer must
always have a valid
email address.”
$customerValidator = new CustomerValidator;!
if($customerValidator->isValid($customer)){!
// ...!
}
Test fails

class CustomerTest extends PHPUnit_Framework_TestCase!
{!
/** @test */!
public function should_always_have_a_v...
Test passes

final class Customer !
{!
public function __construct($email)!
{!
if( /* boring validation stuff */) {!
throw...
Violates
Single Responsibility
Principle
Test passes

final class Email!
{!
private $email;!
!

public function __construct($email)!
{!
if( /* boring validation st...
Test passes

final class Customer!
{!
/** @var Email */!
private $email;!
!

public function __construct(Email $email)!
{!...
Test passes

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

Equality by
Identity
Lifecycle
Mutable

Value
Object
Equality by
Value
!

Immutable
Encapsulate
state and behavior
with Value Objects
The domain expert says

“A customer
orders products
and pays for them.”
$order = new Order;!
$order->setCustomer($customer);!
$order->setProducts($products);!
$order->setStatus(Order::UNPAID);!
...
$order = new Order;!
$order->setCustomer($customer);!
$order->setProducts($products);!
$order->setStatus(!
new PaymentStat...
$order = new Order;!
$order->setCustomer($customer);!
$order->setProducts($products);!
$order->setStatus(!
new PaymentStat...
$order = new Order($customer, $products);!
// set PaymentStatus in Order::__construct()!
!
!
!
!
!
!
!

$order->setPaidMon...
$order = new Order($customer, $products);!
!
!
!
!
!
!
!
!

$order->pay(!
new Money(500, new Currency(‘EUR’))!
);!
// set ...
Encapsulate operations
$order = $customer->order($products);!
!
!
!
!
!
!
!
!

$customer->payFor(!
$order,!
new Money(500, new Currency(‘EUR’))!
...
The domain expert says

“Premium customers
get special offers.”
if($customer->isPremium()) {!
// send special offer!
}
The domain expert says

“Order 3 times
to become a
premium customer.”
interface CustomerSpecification !
{!
/** @return bool */!
public function isSatisfiedBy(Customer $customer); !
}
class CustomerIsPremium implements CustomerSpecification !
{!
private $orderRepository;!
public function __construct(!
Ord...
$customerIsPremium = new CustomerIsPremium;!
!

$aCustomerWith2Orders = ...!
$aCustomerWith3Orders = ...!
!

assertFalse(!...
The domain expert says

“Different rules apply
for different tenants.”
interface CustomerIsPremium !
extends CustomerSpecification!
!

final class CustomerWith3OrdersIsPremium !
implements Cust...
final class SpecialOfferSender!
{!
private $customerIsPremium;!
!
!

public function __construct(!
CustomerIsPremium $cust...
!

<!-- if you load services_amazon.xml: -->!
<service id="customer.is.premium"!
class="CustomerWith500EuroTotalIsPremium"...
Use specifications to
encapsulate rules
about object selection
The domain expert says

“Get a list of
all premium
customers.”
interface CustomerRepository!
{!
public function add(Customer $customer);!
!

public function remove(Customer $customer);!...
Use repositories to
create the illusion of
in-memory collections
interface CustomerRepository!
{!
!

/** @return Customer[] */!
public function findSatisfying(!
CustomerSpecification $cus...
class DbCustomerRepository implements CustomerRepository!
{!
/** @return Customer[] */!
public function findSatisfying(!
C...
final class CustomerWith3OrdersIsPremium!
implements CustomerSpecification!
{!
public function asSql() {!
return ‘SELECT *...
Use double dispatch
to preserve encapsulation
$expectedCustomers = array_filter(!
$repository->findAll(),!
// filter…!
);!
!

$actualCustomers = !
$repository->findSati...
Test by comparing
different representations
of the same rule
Protect your invariants
!

Objects as
consistency boundaries
!

Encapsulate
state and behavior
Thanks! Questions?
I ♥ Feedback	

joind.in/10690
!

Blog, Slides, other talks:	

verraes.net	

@mathiasverraes
Upcoming SlideShare
Loading in …5
×

Unbreakable Domain Models PHPUK 2014 London

1,202 views

Published on

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.

Published in: Technology
0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,202
On SlideShare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
14
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

Unbreakable Domain Models PHPUK 2014 London

  1. 1. Unbreakable Domain Models @mathiasverraes
  2. 2. A Map of the World
  3. 3. London 3h train rides Kortrijk, Belgium Paris Amsterdam
  4. 4. All models are wrong, but some are useful.
  5. 5. I’m Mathias Verraes I'm an independent consultant. I help teams build enterprise web applications.
  6. 6. Blog verraes.net ! Podcast with @everzet elephantintheroom.io ! DDD in PHP bit.ly/dddinphp
  7. 7. Domain Problem Space Domain Model Solution Space
  8. 8. Data Model ~= Structural Model ~= State ! Domain Model ~= Behavioral Model !
  9. 9. Protect your invariants
  10. 10. The domain expert says “A customer must always have an email address.” * Could be different for your domain ** All examples are simplified
  11. 11. Test fails class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function should_always_have_an_email()! {! ! $customer = new Customer();! ! assertThat(! $customer->getEmail(),! equalTo('jim@example.com') ! );! ! }! }
  12. 12. Test passes 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') ! );! }! }
  13. 13. Test fails 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’);! ! }! }
  14. 14. final class Customer! {! private $email;! ! public function __construct($email)! {! $this->email = $email;! }! ! public function getEmail()! {! return $this->email;! }! }
  15. 15. Test passes 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') ! );! }! }
  16. 16. Use objects as consistency boundaries
  17. 17. final class ProspectiveCustomer ! {! public function __construct()! {! // no email! }! }! ! final class PayingCustomer ! { ! public function __construct($email)! {! $this->email = $email;! }! }
  18. 18. Make the implicit explicit
  19. 19. final class ProspectiveCustomer ! {! /** @return PayingCustomer */! public function convertToPayingCustomer($email)! { ! //...! }! }! ! final class PayingCustomer ! { ! //...! }
  20. 20. The domain expert meant “A customer must always have a valid email address.”
  21. 21. $customerValidator = new CustomerValidator;! if($customerValidator->isValid($customer)){! // ...! }
  22. 22. Test fails class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function should_always_have_a_valid_email()! {! ! $this->setExpectedException(! 'InvalidArgumentException'! );! ! new Customer('malformed@email');! ! }! }
  23. 23. Test passes final class Customer ! {! public function __construct($email)! {! if( /* boring validation stuff */) {! throw new InvalidArgumentException();! }! $this->email = $email;! }! }
  24. 24. Violates Single Responsibility Principle
  25. 25. Test passes 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;! } ! }
  26. 26. Test passes final class Customer! {! /** @var Email */! private $email;! ! public function __construct(Email $email)! {! $this->email = $email;! }! }
  27. 27. Test passes class CustomerTest extends PHPUnit_Framework_TestCase! {! /** @test */! public function should_always_have_a_valid_email()! {! ! $this->setExpectedException(! ‘InvalidArgumentException’! );! ! new Customer(new Email(‘malformed@email’));! ! }! }
  28. 28. Entity ! Equality by Identity Lifecycle Mutable Value Object Equality by Value ! Immutable
  29. 29. Encapsulate state and behavior with Value Objects
  30. 30. The domain expert says “A customer orders products and pays for them.”
  31. 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. 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. 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. 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. 35. $order = new Order($customer, $products);! ! ! ! ! ! ! ! ! $order->pay(! new Money(500, new Currency(‘EUR’))! );! // set PaymentStatus in Order#pay()! !
  36. 36. Encapsulate operations
  37. 37. $order = $customer->order($products);! ! ! ! ! ! ! ! ! $customer->payFor(! $order,! new Money(500, new Currency(‘EUR’))! );! !
  38. 38. The domain expert says “Premium customers get special offers.”
  39. 39. if($customer->isPremium()) {! // send special offer! }
  40. 40. The domain expert says “Order 3 times to become a premium customer.”
  41. 41. interface CustomerSpecification ! {! /** @return bool */! public function isSatisfiedBy(Customer $customer); ! }
  42. 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. 43. $customerIsPremium = new CustomerIsPremium;! ! $aCustomerWith2Orders = ...! $aCustomerWith3Orders = ...! ! assertFalse(! $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders)! );! ! assertTrue(! $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders)! );! ! !
  44. 44. The domain expert says “Different rules apply for different tenants.”
  45. 45. interface CustomerIsPremium ! extends CustomerSpecification! ! final class CustomerWith3OrdersIsPremium ! implements CustomerIsPremium! ! final class CustomerWith500EuroTotalIsPremium! implements CustomerIsPremium! ! final class CustomerWhoBoughtLuxuryProductsIsPremium! implements CustomerIsPremium! ! ...!
  46. 46. final class SpecialOfferSender! {! private $customerIsPremium;! ! ! public function __construct(! CustomerIsPremium $customerIsPremium) {...}! ! ! public function sendOffersTo(Customer $customer) ! {! if($this->customerIsPremium->isSatisfiedBy(! $customer! )) ! {! // send offers...! }! }! }!
  47. 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. 48. Use specifications to encapsulate rules about object selection
  49. 49. The domain expert says “Get a list of all premium customers.”
  50. 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. 51. Use repositories to create the illusion of in-memory collections
  52. 52. interface CustomerRepository! {! ! /** @return Customer[] */! public function findSatisfying(! CustomerSpecification $customerSpecification! );! ! }! ! ! // generalized:! $objects = $repository->findSatisfying($specification);!
  53. 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. 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. 55. Use double dispatch to preserve encapsulation
  56. 56. $expectedCustomers = array_filter(! $repository->findAll(),! // filter…! );! ! $actualCustomers = ! $repository->findSatisfying($specification);! ! assertThat($expectedCustomers, equalTo($actualCustomers));
  57. 57. Test by comparing different representations of the same rule
  58. 58. Protect your invariants ! Objects as consistency boundaries ! Encapsulate state and behavior
  59. 59. Thanks! Questions? I ♥ Feedback joind.in/10690 ! Blog, Slides, other talks: verraes.net @mathiasverraes

×