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
8. The domain expert says
“A customer must
always have an
email address.”
* Could be different for your domain
** All examples are simplified
9. class CustomerTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function should_always_have_an_email()
{
$customer = new Customer();
assertThat(
$customer->getEmail(),
equalTo('jim@example.com')
);
}
}
Test fails
10. class CustomerTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function should_always_have_an_email()
{
$customer = new Customer();
$customer->setEmail('jim@example.com');
assertThat(
$customer->getEmail(),
equalTo('jim@example.com')
);
}
}
Test passes
11. class CustomerTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function should_always_have_an_email()
{
$customer = new Customer();
assertThat(
$customer->getEmail(),
equalTo(‘jim@example.com')
);
$customer->setEmail(‘jim@example.com’);
}
}
Test fails
12. class Customer
{
private $email;
public function __construct($email)
{
$this->email = $email;
}
public function getEmail()
{
return $this->email;
}
}
Test passes
13. class CustomerTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function should_always_have_an_email()
{
$customer = new Customer(‘jim@example.com’);
assertThat(
$customer->getEmail(),
equalTo(‘jim@example.com')
);
}
}
Test passes
17. The domain expert meant
“A customer must
always have a valid
email address.”
18. $customerValidator = new CustomerValidator;
if($customerValidator->isValid($customer)){
// ...
}
19. class CustomerTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function should_always_have_a_valid_email()
{
$this->setExpectedException(
'InvalidArgumentException'
);
new Customer('malformed@email');
}
}
Test fails
20. class Customer
{
public function __construct($email)
{
if( /* ugly regex here */) {
throw new InvalidArgumentException();
}
$this->email = $email;
}
}
Test passes
22. 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
23. class Customer
{
/** @var Email */
private $email;
public function __construct(Email $email)
{
$this->email = $email;
}
}
Test passes
24. 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
28. $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)
);
29. $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)
);
30. $order = new Order($customer, $products);
// set PaymentStatus in Order::__construct()
$order->setPaidMonetary(
new Money(500, new Currency(‘EUR’))
);
$order->setStatus(
new PaymentStatus(PaymentStatus::PAID)
);
31. $order = new Order($customer, $products);
$order->pay(
new Money(500, new Currency(‘EUR’))
);
// set PaymentStatus in Order#pay()
46. 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);
}
47. interface CustomerRepository
{
/** @return Customer[] */
public function findSatisfying(
CustomerSpecification $customerSpecification
);
}
// generalized:
$objects = $repository->findSatisfying($specification);
48. class DbCustomerRepository implements CustomerRepository
{
/** @return Customer[] */
public function findSatisfying(
CustomerSpecification $customerSpecification)
{
// filter Customers (see next slide)
}
}
49. // class DbCustomerRepository
public function findSatisfying($specification)
{
$foundCustomers = array();
foreach($this->findAll() as $customer) {
if($specification->isSatisfiedBy($customer)) {
$foundCustomers[] = $customer;
}
}
return $foundCustomers;
}