Slide 1

Slide 1 text

Unbreakable Domain Models @mathiasverraes

Slide 2

Slide 2 text

A Map of the World

Slide 3

Slide 3 text

London Paris Amsterdam Kortrijk, Belgium 3h train rides

Slide 4

Slide 4 text

All models are wrong, but some are useful.

Slide 5

Slide 5 text

I'm an independent consultant. I help teams build enterprise web applications. I’m Mathias Verraes

Slide 6

Slide 6 text

Blog verraes.net ! Podcast with @everzet elephantintheroom.io ! DDD in PHP bit.ly/dddinphp

Slide 7

Slide 7 text

Domain Problem Space Domain Model Solution Space

Slide 8

Slide 8 text

Data Model ~= Structural Model ~= State ! Domain Model ~= Behavioral Model !

Slide 9

Slide 9 text

Protect your invariants

Slide 10

Slide 10 text

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

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]') ! );! ! }! } Test fails

Slide 12

Slide 12 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 13

Slide 13 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 14

Slide 14 text

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

Slide 15

Slide 15 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 16

Slide 16 text

Use objects as consistency boundaries

Slide 17

Slide 17 text

final class ProspectiveCustomer ! {! public function __construct()! {! // no email! }! }! ! final class PayingCustomer ! { ! public function __construct($email)! {! $this->email = $email;! }! }

Slide 18

Slide 18 text

Make the implicit explicit

Slide 19

Slide 19 text

final class ProspectiveCustomer ! {! /** @return PayingCustomer */! public function convertToPayingCustomer($email)! { ! //...! }! }! ! final class PayingCustomer ! { ! //...! }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 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 23

Slide 23 text

final class Customer ! {! public function __construct($email)! {! if( /* boring validation stuff */) {! throw new \InvalidArgumentException();! }! $this->email = $email;! }! } Test passes

Slide 24

Slide 24 text

Violates Single Responsibility Principle

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 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 28

Slide 28 text

Entity ! Equality by Identity Lifecycle Mutable Value Object Equality by Value ! Immutable

Slide 29

Slide 29 text

Encapsulate state and behavior with Value Objects

Slide 30

Slide 30 text

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

Slide 31

Slide 31 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 32

Slide 32 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 33

Slide 33 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 34

Slide 34 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 35

Slide 35 text

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

Slide 36

Slide 36 text

Encapsulate operations

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 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 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

! ! ! ! ! ! ! ! ! ! !

Slide 48

Slide 48 text

Use specifications to encapsulate rules about object selection

Slide 49

Slide 49 text

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

Slide 50

Slide 50 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 51

Slide 51 text

Use repositories to create the illusion of in-memory collections

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Use double dispatch to preserve encapsulation

Slide 56

Slide 56 text

$expectedCustomers = array_filter(! $repository->findAll(),! // filter…! );! ! $actualCustomers = ! $repository->findSatisfying($specification);! ! assertThat($expectedCustomers, equalTo($actualCustomers));

Slide 57

Slide 57 text

Test by comparing different representations of the same rule

Slide 58

Slide 58 text

Protect your invariants ! Objects as consistency boundaries ! Encapsulate state and behavior

Slide 59

Slide 59 text

Thanks! Questions? ! Blog, Slides, other talks: verraes.net @mathiasverraes I ♥ Feedback joind.in/10690