Slide 1

Slide 1 text

Unbreakable Domain Models Mathias Verraes FrOSCon Sankt-Augustin, DE August 24, 2013 @mathiasverraes http://verraes.net

Slide 2

Slide 2 text

I'm an independent consultant. I build enterprise web applications.

Slide 3

Slide 3 text

I help teams escape from survival mode.

Slide 4

Slide 4 text

Cofounder of the Belgian Domain-Driven Design community http://domaindriven.be @DDDBE Modellathon on September 3rd, 2013 Ghent

Slide 5

Slide 5 text

Domain Problem Space Domain Model Solution Space

Slide 6

Slide 6 text

(Data Model The model’s state)

Slide 7

Slide 7 text

Protect your invariants

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Use objects as consistency boundaries

Slide 15

Slide 15 text

class ProspectiveCustomer { //... /** @return PayingCustomer */ public function convertToPayingCustomer(){ } } class PayingCustomer { ... }

Slide 16

Slide 16 text

Make the implicit explicit

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Violates Single Responsibility Principle

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Encapsulate state and behavior with Value Objects

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

$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) );

Slide 29

Slide 29 text

$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) );

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Encapsulate operations

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Slide 44

Slide 44 text

Use specifications to encapsulate rules about object selection

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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); }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Use double dispatch to preserve encapsulation

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Test by comparing different representations

Slide 54

Slide 54 text

Protect your invariants Objects as consistency boundaries Encapsulate state and behavior

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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