unbreakable domain models - froscon 2013

56
Unbreakable Domain Models Mathias Verraes FrOSCon Sankt-Augustin, DE August 24, 2013 @mathiasverraes http://verraes.net

Upload: mathiasverraes

Post on 11-May-2015

723 views

Category:

Technology


0 download

DESCRIPTION

DataMappers 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 prevent our code from abuse? - Where to put queries, and how test them? It’s time to look beyond the old Gang of Four design patterns. There are Value Objects, Entities and Aggregates at the core; Repositories for persistence; Specifications to accurately describe object selections; Encapsulated Operations to protect invariants; and Domain Services and Double Dispatch when we need to group behavior safely. These patterns help us evolve from structural data models, to rich behavioral models. They capture not just state and relationships, but true meaning. These patterns protect our models from being used incorrectly, and allow us to test the essence of our applications. The presentation is a fast paced introduction to the patterns that will make your Domain Model expressive, unbreakable, and beautiful. More at http://verraes.net/ or http://twitter.com/mathiasverraes

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

...