Unbreakable Domain Models @mathiasverraes
A Map of the World
London
Paris Amsterdam
Kortrijk, Belgium
3h train rides
All models are wrong, but some are useful.
I'm an independent consultant.
I help teams build enterprise web applications.
I’m Mathias Verraes
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 examples are simplified
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
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
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
final class Customer!{! private $email;!! public function __construct($email)! {! $this->email = $email;! }!! public function getEmail()! {! return $this->email;! }!}
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
Use objects as consistency boundaries
final class ProspectiveCustomer !{! public function __construct()! {! // no email! }!}!!final class PayingCustomer !{ ! public function __construct($email)! {! $this->email = $email;! }!}
Make the implicit explicit
final class ProspectiveCustomer !{! /** @return PayingCustomer */! public function convertToPayingCustomer($email)! { ! //...! }!}!!final class PayingCustomer !{ ! //...!}
The domain expert meant
“A customer must always have a valid
email address.”
$customerValidator = new CustomerValidator;!if($customerValidator->isValid($customer)){! // ...!}
class CustomerTest extends PHPUnit_Framework_TestCase!{! /** @test */! public function should_always_have_a_valid_email()! {!! $this->setExpectedException(! '\InvalidArgumentException'! );!! new Customer('malformed@email');!! }!}
Test fails
final class Customer !{! public function __construct($email)! {! if( /* boring validation stuff */) {! throw new \InvalidArgumentException();! }! $this->email = $email;! }!}
Test passes
Violates Single Responsibility
Principle
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
final class Customer!{! /** @var Email */! private $email;!! public function __construct(Email $email)! {! $this->email = $email;! }!}
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’));!! }!}
Test passes
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->setPaidAmount(500);!$order->setPaidCurrency(‘EUR’);!!$order->setStatus(Order::PAID);!!
$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)!);
$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)!);
$order = new Order($customer, $products);!// set PaymentStatus in Order::__construct()!!!!!!!!$order->setPaidMonetary(! new Money(500, new Currency(‘EUR’))!);!$order->setStatus(! new PaymentStatus(PaymentStatus::PAID)!);
$order = new Order($customer, $products);!!!!!!!!!$order->pay(! new Money(500, new Currency(‘EUR’))!);!// set PaymentStatus in Order#pay()!!
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(! 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!}!
$customerIsPremium = new CustomerIsPremium;!!$aCustomerWith2Orders = ...!$aCustomerWith3Orders = ...!!assertFalse(! $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders)!);!!assertTrue(! $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders)!);!!!
The domain expert says
“Different rules apply for different tenants.”
interface CustomerIsPremium ! extends CustomerSpecification!!final class CustomerWith3OrdersIsPremium ! implements CustomerIsPremium!!final class CustomerWith500EuroTotalIsPremium! implements CustomerIsPremium!!final class CustomerWhoBoughtLuxuryProductsIsPremium! implements CustomerIsPremium!!...!
final class SpecialOfferSender!{! private $customerIsPremium;!!! public function __construct(! CustomerIsPremium $customerIsPremium) {...}!!! public function sendOffersTo(Customer $customer) ! {! if($this->customerIsPremium->isSatisfiedBy(! $customer! )) ! {! // send offers...! }! }!}!
!<!-- 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>
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);! ! /** @return Customer */! public function find(CustomerId $customerId);!! /** @return Customer[] */! public function findAll();!! /** @return Customer[] */! public function findRegisteredIn(Year $year);!}!
Use repositories to create the illusion of
in-memory collections
interface CustomerRepository!{!! /** @return Customer[] */! public function findSatisfying(! CustomerSpecification $customerSpecification! );!!}!!!// generalized:!$objects = $repository->findSatisfying($specification);!
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);! } ! );!! }!}!
final class CustomerWith3OrdersIsPremium! implements CustomerSpecification!{! public function asSql() {! return ‘SELECT * FROM Customer...’;! }!}!!!// class DbCustomerRepository !public function findSatisfying($specification) !{! return $this->db->query($specification->asSql()); !}
Use double dispatch to preserve encapsulation
$expectedCustomers = array_filter(! $repository->findAll(),! // filter…!);!!$actualCustomers = ! $repository->findSatisfying($specification);!!assertThat($expectedCustomers, equalTo($actualCustomers));
Test by comparing different representations
of the same rule
Protect your invariants !
Objects as consistency boundaries
!
Encapsulate state and behavior
Thanks! Questions?
!
Blog, Slides, other talks: verraes.net
@mathiasverraes
I ♥ Feedback joind.in/10690
Top Related