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