Transcript
Page 1: Unbreakable Domain Models - FrOSCon 2013

UnbreakableDomain Models

Mathias Verraes

FrOSConSankt-Augustin, DE

August 24, 2013

@mathiasverraeshttp://verraes.net

Page 2: Unbreakable Domain Models - FrOSCon 2013

I'm an independent consultant.

I build enterprise web applications.

Page 3: Unbreakable Domain Models - FrOSCon 2013

I help teams escape from

survival mode.

Page 4: Unbreakable Domain Models - FrOSCon 2013

Cofounder of theBelgian

Domain-Driven Design community

http://domaindriven.be@DDDBE

Modellathon on September 3rd, 2013

Ghent

Page 5: Unbreakable Domain Models - FrOSCon 2013

DomainProblem Space

Domain ModelSolution Space

Page 6: Unbreakable Domain Models - FrOSCon 2013

(Data ModelThe model’s state)

Page 7: Unbreakable Domain Models - FrOSCon 2013

Protect your invariants

Page 8: Unbreakable Domain Models - FrOSCon 2013

The domain expert says

“A customer must always have an email address.”

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

Page 9: Unbreakable Domain Models - FrOSCon 2013

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 10: Unbreakable Domain Models - FrOSCon 2013

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 11: Unbreakable Domain Models - FrOSCon 2013

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 12: Unbreakable Domain Models - FrOSCon 2013

class Customer{ private $email;

public function __construct($email) { $this->email = $email; }

public function getEmail() { return $this->email; }}

Test passes

Page 13: Unbreakable Domain Models - FrOSCon 2013

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 14: Unbreakable Domain Models - FrOSCon 2013

Use objects asconsistency boundaries

Page 15: Unbreakable Domain Models - FrOSCon 2013

class ProspectiveCustomer { //...

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

class PayingCustomer { ... }

Page 16: Unbreakable Domain Models - FrOSCon 2013

Make the implicitexplicit

Page 17: Unbreakable Domain Models - FrOSCon 2013

The domain expert meant

“A customer must always have a valid

email address.”

Page 18: Unbreakable Domain Models - FrOSCon 2013

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

Page 19: Unbreakable Domain Models - FrOSCon 2013

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 20: Unbreakable Domain Models - FrOSCon 2013

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

Test passes

Page 21: Unbreakable Domain Models - FrOSCon 2013

ViolatesSingle Responsibility

Principle

Page 22: Unbreakable Domain Models - FrOSCon 2013

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

Page 23: Unbreakable Domain Models - FrOSCon 2013

class Customer{ /** @var Email */ private $email;

public function __construct(Email $email) { $this->email = $email; }}

Test passes

Page 24: Unbreakable Domain Models - FrOSCon 2013

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 25: Unbreakable Domain Models - FrOSCon 2013

Encapsulate state and behaviorwith Value Objects

Page 26: Unbreakable Domain Models - FrOSCon 2013

The domain expert says

“A customer orders products

and pays for them.”

Page 27: Unbreakable Domain Models - FrOSCon 2013

$order = new Order;$order->setCustomer($customer);$order->setProducts($products);$order->setStatus(Order::UNPAID);

// ...

$order->setPaidAmount(500);$order->setPaidCurrency(‘EUR’);

$order->setStatus(Order::PAID);

Page 28: Unbreakable Domain Models - FrOSCon 2013

$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 29: Unbreakable Domain Models - FrOSCon 2013

$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 30: Unbreakable Domain Models - FrOSCon 2013

$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 31: Unbreakable Domain Models - FrOSCon 2013

$order = new Order($customer, $products);

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

Page 32: Unbreakable Domain Models - FrOSCon 2013

Encapsulate operations

Page 33: Unbreakable Domain Models - FrOSCon 2013

$order = $customer->order($products);

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

Page 34: Unbreakable Domain Models - FrOSCon 2013

The domain expert says

“Premium customers get special offers.”

Page 35: Unbreakable Domain Models - FrOSCon 2013

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

Page 36: Unbreakable Domain Models - FrOSCon 2013

The domain expert says

“Order 3 times to become a

premium customer.”

Page 37: Unbreakable Domain Models - FrOSCon 2013

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

Page 38: Unbreakable Domain Models - FrOSCon 2013

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 39: Unbreakable Domain Models - FrOSCon 2013

$customerIsPremium = new CustomerIsPremium;

$aCustomerWith2Orders = ...$aCustomerWith3Orders = ...

assertFalse( $customerIsPremium->isSatisfiedBy($aCustomerWith2Orders));

assertTrue( $customerIsPremium->isSatisfiedBy($aCustomerWith3Orders));

Page 40: Unbreakable Domain Models - FrOSCon 2013

The domain expert says

“Different rules apply for different tenants.”

Page 41: Unbreakable Domain Models - FrOSCon 2013

interface CustomerIsPremium extends CustomerSpecification

class CustomerWith3OrdersIsPremium implements CustomerIsPremium

class CustomerWith500EuroTotalIsPremium implements CustomerIsPremium

class CustomerWhoBoughtLuxuryProductsIsPremium implements CustomerIsPremium

...

Page 42: Unbreakable Domain Models - FrOSCon 2013

class SpecialOfferSender{ private $customerIsPremium;

public function __construct( CustomerIsPremium $customerIsPremium) {...}

public function sendOffersTo(Customer $customer) { if($this->customerIsPremium->isSatisfiedBy( $customer )) { // send offers... } }}

Page 43: Unbreakable Domain Models - FrOSCon 2013

<!-- 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 44: Unbreakable Domain Models - FrOSCon 2013

Use specifications to encapsulate rules

about object selection

Page 45: Unbreakable Domain Models - FrOSCon 2013

The domain expert says

“Get a list of all premium customers.”

Page 46: Unbreakable Domain Models - FrOSCon 2013

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 47: Unbreakable Domain Models - FrOSCon 2013

interface CustomerRepository{

/** @return Customer[] */ public function findSatisfying( CustomerSpecification $customerSpecification );

}

// generalized:$objects = $repository->findSatisfying($specification);

Page 48: Unbreakable Domain Models - FrOSCon 2013

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

Page 49: Unbreakable Domain Models - FrOSCon 2013

// class DbCustomerRepository

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

Page 50: Unbreakable Domain Models - FrOSCon 2013

class CustomerWith3OrdersIsPremium implements CustomerSpecification{ public function asSql() { return ‘SELECT * FROM Customer...’; }}

// class DbCustomerRepository public function findSatisfying($specification) { return $this->db->query($specification->asSql()); }

Page 51: Unbreakable Domain Models - FrOSCon 2013

Use double dispatchto preserve encapsulation

Page 52: Unbreakable Domain Models - FrOSCon 2013

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

assertThat($expectedCustomers, equalTo($actualCustomers));

Page 53: Unbreakable Domain Models - FrOSCon 2013

Test by comparing different representations

Page 54: Unbreakable Domain Models - FrOSCon 2013

Protect your invariants

Objects as consistency boundaries

Encapsulate state and behavior

Page 55: Unbreakable Domain Models - FrOSCon 2013

More? google for:Eric Evans

Vaugh VernonMartin Fowler

Greg YoungUdi Dahan

Sandro MarcusoYves Reynhout

Szymon PobiegaAlberto Brandolini

...


Top Related