Unbreakable
Domain Models
Mathias Verraes
FrOSCon
Sankt-Augustin, DE
August 24, 2013
@mathiasverraes
http://verraes.net
Slide 2
Slide 2 text
I'm an independent
consultant.
I build enterprise
web applications.
Slide 3
Slide 3 text
I help teams
escape from
survival mode.
Slide 4
Slide 4 text
Cofounder of the
Belgian
Domain-Driven
Design community
http://domaindriven.be
@DDDBE
Modellathon on
September 3rd, 2013
Ghent
Slide 5
Slide 5 text
Domain
Problem Space
Domain Model
Solution Space
Slide 6
Slide 6 text
(Data Model
The model’s state)
Slide 7
Slide 7 text
Protect your invariants
Slide 8
Slide 8 text
The domain expert says
“A customer must
always have an
email address.”
* Could be different for your domain
** All examples are simplified
Slide 9
Slide 9 text
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
Slide 10
Slide 10 text
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
Slide 11
Slide 11 text
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
Slide 12
Slide 12 text
class Customer
{
private $email;
public function __construct($email)
{
$this->email = $email;
}
public function getEmail()
{
return $this->email;
}
}
Test passes
Slide 13
Slide 13 text
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
Slide 14
Slide 14 text
Use objects as
consistency boundaries
Slide 15
Slide 15 text
class ProspectiveCustomer
{
//...
/** @return PayingCustomer */
public function convertToPayingCustomer(){
}
}
class PayingCustomer { ... }
Slide 16
Slide 16 text
Make the implicit
explicit
Slide 17
Slide 17 text
The domain expert meant
“A customer must
always have a valid
email address.”
Slide 18
Slide 18 text
$customerValidator = new CustomerValidator;
if($customerValidator->isValid($customer)){
// ...
}
Slide 19
Slide 19 text
class CustomerTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function should_always_have_a_valid_email()
{
$this->setExpectedException(
'\InvalidArgumentException'
);
new Customer('malformed@email');
}
}
Test fails
Slide 20
Slide 20 text
class Customer
{
public function __construct($email)
{
if( /* ugly regex here */) {
throw new \InvalidArgumentException();
}
$this->email = $email;
}
}
Test passes
Slide 21
Slide 21 text
Violates
Single Responsibility
Principle
Slide 22
Slide 22 text
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
Slide 23
Slide 23 text
class Customer
{
/** @var Email */
private $email;
public function __construct(Email $email)
{
$this->email = $email;
}
}
Test passes
Slide 24
Slide 24 text
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
Slide 25
Slide 25 text
Encapsulate
state and behavior
with Value Objects
Slide 26
Slide 26 text
The domain expert says
“A customer
orders products
and pays for them.”
$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)
);
Slide 29
Slide 29 text
$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)
);
Slide 30
Slide 30 text
$order = new Order($customer, $products);
// set PaymentStatus in Order::__construct()
$order->setPaidMonetary(
new Money(500, new Currency(‘EUR’))
);
$order->setStatus(
new PaymentStatus(PaymentStatus::PAID)
);
Slide 31
Slide 31 text
$order = new Order($customer, $products);
$order->pay(
new Money(500, new Currency(‘EUR’))
);
// set PaymentStatus in Order#pay()
Slide 32
Slide 32 text
Encapsulate operations
Slide 33
Slide 33 text
$order = $customer->order($products);
$customer->pay(
$order,
new Money(500, new Currency(‘EUR’))
);
Slide 34
Slide 34 text
The domain expert says
“Premium customers
get special offers.”
Slide 35
Slide 35 text
if($customer->isPremium()) {
// send special offer
}
Slide 36
Slide 36 text
The domain expert says
“Order 3 times
to become a
premium customer.”
Slide 37
Slide 37 text
interface CustomerSpecification
{
/** @return bool */
public function isSatisfiedBy(Customer $customer);
}
Slide 38
Slide 38 text
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
}
The domain expert says
“Different rules apply
for different tenants.”
Slide 41
Slide 41 text
interface CustomerIsPremium
extends CustomerSpecification
class CustomerWith3OrdersIsPremium
implements CustomerIsPremium
class CustomerWith500EuroTotalIsPremium
implements CustomerIsPremium
class CustomerWhoBoughtLuxuryProductsIsPremium
implements CustomerIsPremium
...
Slide 42
Slide 42 text
class SpecialOfferSender
{
private $customerIsPremium;
public function __construct(
CustomerIsPremium $customerIsPremium) {...}
public function sendOffersTo(Customer $customer)
{
if($this->customerIsPremium->isSatisfiedBy(
$customer
))
{
// send offers...
}
}
}
Slide 43
Slide 43 text
Slide 44
Slide 44 text
Use specifications to
encapsulate rules
about object selection
Slide 45
Slide 45 text
The domain expert says
“Get a list of
all premium
customers.”
Slide 46
Slide 46 text
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);
}
Slide 47
Slide 47 text
interface CustomerRepository
{
/** @return Customer[] */
public function findSatisfying(
CustomerSpecification $customerSpecification
);
}
// generalized:
$objects = $repository->findSatisfying($specification);
Slide 48
Slide 48 text
class DbCustomerRepository implements CustomerRepository
{
/** @return Customer[] */
public function findSatisfying(
CustomerSpecification $customerSpecification)
{
// filter Customers (see next slide)
}
}
Slide 49
Slide 49 text
// class DbCustomerRepository
public function findSatisfying($specification)
{
$foundCustomers = array();
foreach($this->findAll() as $customer) {
if($specification->isSatisfiedBy($customer)) {
$foundCustomers[] = $customer;
}
}
return $foundCustomers;
}
Slide 50
Slide 50 text
class CustomerWith3OrdersIsPremium
implements CustomerSpecification
{
public function asSql() {
return ‘SELECT * FROM Customer...’;
}
}
// class DbCustomerRepository
public function findSatisfying($specification)
{
return $this->db->query($specification->asSql());
}