Unbreakable Domain Models

Notes from PHP UK Conference 2014 - Mathias Verraes

  • The Domain - the problem the business is trying to solve
  • The Domain model - implementation of this, different to the Data model
  • Domain model is about behaviour, data is a side affect of the behaviour 'I do this to get this done'

Invariants

business rules to make sure there is no way to violate the business rules. write code to make sure users (and developers) can't violate these rules.

Use objects as consistency boundaries.

Users with / without email address: ProspectiveCustomer and PayingCustomer

Make the implicit explicit

Don't use getters and setters

frameworks force us to have setters and getters, there are ways around it: create layers in between:

  • command objects
  • data transport objects

Validation

  • frameworks force us to have validation on the outside
  • this means we can easily forget the validation
  • q: you could put the validation in the object construct but this violates SOLID
  • a: to solve, create an Email address class with the validation - keep this responsibility separate

    value objects are the heart and soul of DDDD

Entity:

  • equality by identity
  • lifecycle
  • mutable eg: a customer: your name may change but you are always the same customer

Value Object (eg Email, School Year (which is different to a calendar year), Distances, Measurements, Datetime, Money, Currency, DateRange Object)

  • Equality by Value
  • immutable eg: 2 email objects may be different instances but if the value is the same they are both equal eg: doesn't have a setter because it never changes, just create a new one eg: daterange object can't go from future to past
    • model concepts as separate objects!

Encapsulate state and behaviour with Value Objects

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

// ...

$order->setPaidAmount(500);
$order->setPaidCurrency('EUR');
$order->setStatus(Order::PAID);

we need to get closer to the domain language. Make it readable to the domain expert.

1) Create a PaymentStatus Value Object

$order->setStatus(new PaymentStatus(PaymentStatus::UNPAID));
$order->setStatus(new PaymentStatus(PaymentStatus::PAID));

2) Create Money Value Object

$order->setPaidMonetary(new Money(500, new Currency('EUR')));

3) an order is always for a customer and a bunch of products. So instead of doing a set on the outside assume it's unpaid and create in a constructor

$order = new Order($customer, $products);
// set PaymentStatus in Order::__construct()

4) Pay

$order->pay(new Money(500, new Currency('EUR')));

Encapsulate Operations when you need to change items in an entity

5) Make Pay better - to get closer to the language.

  • A customer orders a product and a customer pays for an order
  • This is as close as you can get to expressing in the domain language

    $customer->payFor($order, new Money(500, new Currency('EUR')));

  • ! But be careful as we have now coupled the customer object to the order object. So the customer object has to know about how paying and orders work (even though it delegates this to the order object) but we have added responsibility to the customer object.

    If you couple everything to everything you are going to get into trouble

Premium Customers

$customer->isPremium() // boolean flag

Business rule: You have to order 3 times to be a premium customer

  • do we put this inside the customer or order?
  • If you are bridging across multiple entties you'll have to do it on the outside but you still want to encapsulate them

1) Specification Pattern

interface CustomerSpecification {

    public function isSatisfiedBy(Customer $customer);
}

Very simple interface. Takes customer as an argument and returns true or false:

So for premium Customer:

class CustomerIsPremium implements CustomerSpecification
{

    private $orderRepository;

    public function __construct(OrderRepository $orderRepository) {
        ...
    }

    public function isSatisfiedBy(Customer $customer) {

        $count = $his->orderRepository->countFor($customer);
        return $count >= 3;
    }
}

$customerIsPremium = new CustomerIsPremium($orderRepository);
if ($customerIsPremium->isSatisfiedBy($customer)) {
    // send special offer
}

Maybe you would Inject the Order Repository with Dependency Injection

Because this business rule is in one place it is easy to change it (in one place).

And it is easily testable:

$customerIsPremiume = new CustomerIsPremium;

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

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

Domain Expert says 'we have different premium rules for different types of customer'

DRY is not about code duplication it is about knowledge - a single source of truth! We had a single source of truth about what made a customer premium, we can still do that if we have different rules:

Turn the CustomerIsPremium class into an interface, the implementations can be totally different:

interface CustomerIsPremium
    extends CustomerSpecification

final class CustomerWith3OrdersIsPremium
    implments CustomerIsPremium

final class CustomerWith500EuroTotalIsPremium
    implments CustomerIsPremium

final class CustomerWhoBoughtLuxuryProductsIsPremium
    implments CustomerIsPremium

...

Usage:

final class SpecialOfferSender
{
    private $customerIsPremium;

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

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

Dependency Injection in Symfony:

  • depending on the customer/tenant string we create a differnet object but give it the same key. Each one needs a customer.is.premium object:

  • amazon customers:

  • ebay customers:

  • others

Use specifications to encapsulate rules about object selection

Domain Expert says: Get a list of premium customers

  • Repository is something that is persistence orientated. E.g Getting objects out of a database, putting objects into the database. But this structural/persistance way of thinking.

Better approach is 'collection orientated repository':

interface CustomerRepository
{

    public function add(Customer $customer);

    public function remove(Customer $customer);

    /** @return Customer */
    public function gind(CustomerId $customerId);

    /** @return Customer[] */
    public function findAll();

    /** @return Customer[] */
    public function findRegisterdIn(Year $year);

}

These are methods that work just as well in in memory collections - the interface would look exactly the same. You would never have methods like persist or save in here. This interface might save objects to the database in the background. But imprtantly you can swap/replace/implement this inteface with an in-memory collection (great for unit tests), a database, or a Doctrine repository or MongoDB repository but the client code still thinks it's a customer repository with collection style methods.

Use repositories to create the illusion of an in-memory collection (even though in background it is persisting to a database).

So to implement Get a list of premium Customers - We already have the specificaiton so we just add a new method to our CustomerRepository, add a findSatisfying method...:

interface CustomerRepository
{

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

    // generalised
    $objects = $repository->findSatisfying($specification);
}

Now we implemet this by looping over the collection and filter:

class DbCustomerRepostory implements CustomerRepository
{

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

        return array_filter(
            $this->findAll(),
            function (Customer $customer) use ($specification) {
                return $specification->isSatisfiedBy($customer);
                }
            );
    }
}
  • we could use a foreach loop instead of array_filter

We have now enforced our business rule using this specification. But this won't be very efficient if you have a lot of customers! We need a proper database query.

It could be quite simple SELECT count(*) from order....

final class CustomerWith3OrdersIsPremium implments CustomerSpecification {

        public function asSql() {
            return '<SELECT></SELECT>LCT * from Customer...';
        }

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

So we have replaced array_filter method with asSql() or dQL for Doctrine etc

This is called Double Dispatch to preserve encapsulation.

On the outside we are telling the repository to give us all the customers satisfying this specification, on the inside the repository says - ok Specification give me your representation in sql and i will us that to query the database.

If you want to get rid of the query and code version of the same business rule there are ways round it but they are not very interesting or useful...

Testing your SQL queries

We still have our in-code nieve array filter as well as our SQL query so it is still easy to test:

$expectedCustomers = array_filter( 
    $repository->findAll(),
    //filter...
);

$actualCustomers = $repository->findSatisfying($specification);

assertThat($expectedCustomers, equalsTo($actualCustomers));
  • Make a Copy of your production data and run a test on it :)

Test by comparing different representations of the same business rule

Rules

  • protect your invariants
  • find the rules that could make your database corrupt
  • find ways to build code where you can't go wrong!
  • objects as consitency boundaries. They are not just placeholders for state, they are the core thing.
  • Encapsulate behaviour

There are always round these rules it (using Reflection or some other black magic! etc) but should be easier to spot!

Links