Transcript
Page 1: Unbreakable Domain Models PHPUK 2014 London

Unbreakable Domain Models @mathiasverraes

Page 2: Unbreakable Domain Models PHPUK 2014 London

A Map of the World

Page 3: Unbreakable Domain Models PHPUK 2014 London

London

Paris Amsterdam

Kortrijk, Belgium

3h train rides

Page 4: Unbreakable Domain Models PHPUK 2014 London

All models are wrong, but some are useful.

Page 5: Unbreakable Domain Models PHPUK 2014 London

I'm an independent consultant.

I help teams build enterprise web applications.

I’m Mathias Verraes

Page 6: Unbreakable Domain Models PHPUK 2014 London

Blog verraes.net

!

Podcast with @everzet elephantintheroom.io

!

DDD in PHP bit.ly/dddinphp

Page 7: Unbreakable Domain Models PHPUK 2014 London

Domain Problem Space

Domain Model Solution Space

Page 8: Unbreakable Domain Models PHPUK 2014 London

Data Model ~= Structural Model ~= State !

Domain Model ~= Behavioral Model !

Page 9: Unbreakable Domain Models PHPUK 2014 London

Protect your invariants

Page 10: Unbreakable Domain Models PHPUK 2014 London

The domain expert says

“A customer must always have an email address.”

* Could be different for your domain ** All examples are simplified

Page 11: Unbreakable Domain Models PHPUK 2014 London

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

Page 12: Unbreakable Domain Models PHPUK 2014 London

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

Page 13: Unbreakable Domain Models PHPUK 2014 London

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

Page 14: Unbreakable Domain Models PHPUK 2014 London

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

Page 15: Unbreakable Domain Models PHPUK 2014 London

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

Page 16: Unbreakable Domain Models PHPUK 2014 London

Use objects as consistency boundaries

Page 17: Unbreakable Domain Models PHPUK 2014 London

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

Page 18: Unbreakable Domain Models PHPUK 2014 London

Make the implicit explicit

Page 19: Unbreakable Domain Models PHPUK 2014 London

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

Page 20: Unbreakable Domain Models PHPUK 2014 London

The domain expert meant

“A customer must always have a valid

email address.”

Page 21: Unbreakable Domain Models PHPUK 2014 London

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

Page 22: Unbreakable Domain Models PHPUK 2014 London

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

Test fails

Page 23: Unbreakable Domain Models PHPUK 2014 London

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

Test passes

Page 24: Unbreakable Domain Models PHPUK 2014 London

Violates Single Responsibility

Principle

Page 25: Unbreakable Domain Models PHPUK 2014 London

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

Page 26: Unbreakable Domain Models PHPUK 2014 London

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

Test passes

Page 27: Unbreakable Domain Models PHPUK 2014 London

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

Page 28: Unbreakable Domain Models PHPUK 2014 London

Entity !

Equality by Identity Lifecycle Mutable

Value Object

Equality by Value

!

Immutable

Page 29: Unbreakable Domain Models PHPUK 2014 London

Encapsulate state and behavior with Value Objects

Page 30: Unbreakable Domain Models PHPUK 2014 London

The domain expert says

“A customer orders products

and pays for them.”

Page 31: Unbreakable Domain Models PHPUK 2014 London

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

Page 32: Unbreakable Domain Models PHPUK 2014 London

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

Page 33: Unbreakable Domain Models PHPUK 2014 London

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

Page 34: Unbreakable Domain Models PHPUK 2014 London

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

Page 35: Unbreakable Domain Models PHPUK 2014 London

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

Page 36: Unbreakable Domain Models PHPUK 2014 London

Encapsulate operations

Page 37: Unbreakable Domain Models PHPUK 2014 London

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

Page 38: Unbreakable Domain Models PHPUK 2014 London

The domain expert says

“Premium customers get special offers.”

Page 39: Unbreakable Domain Models PHPUK 2014 London

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

Page 40: Unbreakable Domain Models PHPUK 2014 London

The domain expert says

“Order 3 times to become a

premium customer.”

Page 41: Unbreakable Domain Models PHPUK 2014 London

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

Page 42: Unbreakable Domain Models PHPUK 2014 London

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

Page 43: Unbreakable Domain Models PHPUK 2014 London

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

Page 44: Unbreakable Domain Models PHPUK 2014 London

The domain expert says

“Different rules apply for different tenants.”

Page 45: Unbreakable Domain Models PHPUK 2014 London

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

Page 46: Unbreakable Domain Models PHPUK 2014 London

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

Page 47: Unbreakable Domain Models PHPUK 2014 London

!<!-- 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>

Page 48: Unbreakable Domain Models PHPUK 2014 London

Use specifications to encapsulate rules

about object selection

Page 49: Unbreakable Domain Models PHPUK 2014 London

The domain expert says

“Get a list of all premium customers.”

Page 50: Unbreakable Domain Models PHPUK 2014 London

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

Page 51: Unbreakable Domain Models PHPUK 2014 London

Use repositories to create the illusion of

in-memory collections

Page 52: Unbreakable Domain Models PHPUK 2014 London

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

Page 53: Unbreakable Domain Models PHPUK 2014 London

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

Page 54: Unbreakable Domain Models PHPUK 2014 London

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

Page 55: Unbreakable Domain Models PHPUK 2014 London

Use double dispatch to preserve encapsulation

Page 56: Unbreakable Domain Models PHPUK 2014 London

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

Page 57: Unbreakable Domain Models PHPUK 2014 London

Test by comparing different representations

of the same rule

Page 58: Unbreakable Domain Models PHPUK 2014 London

Protect your invariants !

Objects as consistency boundaries

!

Encapsulate state and behavior

Page 59: Unbreakable Domain Models PHPUK 2014 London

Thanks! Questions?

!

Blog, Slides, other talks: verraes.net

@mathiasverraes

I ♥ Feedback joind.in/10690