Domain Driven Design

Domain Objects

Domain objects inherit from an abstract domain class which provides encapsulation and commonly required features. Each implements its own interface which is declared in the Core\Domain\iDomain namespace. The interfaces are used in type tests only, to allow substitutes such as proxies and mocks. They inherit from a parent domain interface and there is no need to declare any class features.

Here's an example of a Foo interface.

namespace Core\Domain\iDomain;

interface Foo extends Domain
{ }

Each concrete class has an $allowed array where each key is the name of an allowed property, and each value is a Boolean that declares whether the property is required (true), or optional (false). Actual property values are stored in a protected $data array.

A Foo object with some optional and some required properties may look as follows:

namespace Core\Domain;

class Foo extends AbstractDomain implements iDomain\Foo
{
    protected $allowed = array(
        'id'      => false,
        'name'    => true,
        'address' => false,
        'country' => true
    );
}

Construction

The Domain objects are designed for use with Domain object factories. The factories do most of the heavy lifting and it is rarely necessary for a Domain object to override the constructor of its parent.

The constructor expects an array of values. It cycles through each value, checking first that it is an allowable property, then setting the value of that property. Note that it sets the properties directly rather than using any setter methods that may be available. This works because I have conducted any filtering, validation and transformation prior to construction. (More on filtering and validation later.)

Once the property values are set the constructor checks that all mandatory values have been supplied then stores any factories or finders that have been injected. Factories and finders are sometimes required by behaviour methods.

‘Finder’ is a pseudonym for ‘Data Mapper’. Introducing a dependency from a Domain object to a Data Mapper is breaking the rules, so I do it only with a good reason. I refer to the Mappers here as ‘finders’, since to use them in any other capacity, such as to persist changes, would be a highly questionable violation of the rules.

Access

The property values stored in the protected $data array may be accessed via public methods or overloading only. Overloading is the default case and the magic access methods have different biases. By default:

  • the __get implementation assumes client code can read properties, and
  • the __set implementation assumes client code can not write to properties.

Another way to think of this is that by default, Domain objects are 'open for reading, closed for writing'. To make a property writable an explicit public setter is required and must take the form of setPropertyName($value). Similarly, an explicit getter may be used to hide a property, however in my experience this is rarely required.

In the following example, the country setter would be called by $bar->baz = $value.

namespace Core\Domain;

class Bar extends AbstractDomain implements iDomain\Bar
{
    // ... //

    public function setBaz(iDomain\Baz $baz)
    {
        if ($baz->status !== 'Active') {
            throw new Exception\Exception('Bar->baz must be Active');
        }

        $this-> data['baz'] = $baz;
    }
}

Identity

The identity map, or Domain Watcher, avoids loading the same Domain object in to memory more than once. This protects the integrity of in-memory objects and improves performance. Before attempting to create an object for an existing entity, a factory checks the domain watcher using the entity's identifier. If a matching object exists, it is returned. Data Mappers check the domain watcher too before executing a ‘find’ for a single object.

In the Domain Watcher you will see that my globally unique object identifiers are a combination of the object’s 'short type', and an identifier which must be unique amongst members of the same type. In most cases the identifier is simply an id property on the object, which usually doubles as the primary key in the database. In some cases however it may be another property, such as email, or the identifier of a ‘parent’ object (in the case of object composition).

The abstract Domain object has setId and getId methods for the base case. Note that in these cases the id is marked as optional (in $allowed) since newly created objects are unlikely to have an identity until they have been added to the database. Where objects do not conform to the base case the setId and getId methods should be overridden.

'Value' objects are objects that do not have identities. If your project has only a small number of these you may be able to handle them effectively by avoiding attempts to add them to the domain watcher, and by raising exceptions in their setId and getId methods. If you have a large number however it may be necessary to distinguish them via the object hierarchy. In this case you can declare separate entity and value interfaces, and return early from DomainWatcher::add($object) when $object is not an entity.

Proxies

Most Domain objects have a corresponding proxy class. When a factory constructs a Domain object which composes (or aggregates) other Domain objects, it may not have all the information required to construct these other objects. In such cases the factory may substitute a proxy instead.

Upon construction a Domain object proxy has two real properties: the identifier of the real object, and the Mapper by which the real object may be found. A proxy is realised if/when it is first accessed, and subsequent method calls and property accesses are passed to the real object. This is not the case however when an object is accessed to read its identity only.

When an object’s identity is accessed the proxy returns its own copy of the identifier. This avoids unnecessary database calls for common functions such as testing for object equality, and printing object identifiers in URL lists.

By way of example, here is the code for a Foo proxy.

namespace Core\Domain\Proxy;

use Core\Domain\iDomain;

class Foo extends AbstractProxy implements iDomain\Foo 
{ }

The Domain

The Domain is more involved than other layers of the model. The breadth of its classes is determined by the breadth of the problem the application addresses, which can sometimes be enormous. In addition, for each Domain class there may be one of each of the following:

  • A factory for creating objects of the given type,
  • A proxy for the given type,
  • A collection of the given type, and
  • A proxy for each collection.

Besides the Domain objects and their associates, the Domain namespace also has:

  • An abstract domain superclass with common domain features, and
  • The ‘domain watcher’, which is an implementation of the identity map pattern.

PHP Starter Application

This is a starting point for applications with complex business logic. It is a PHP MVC skeleton with a Domain (Domain Model), Data Mapper and Service Layer. It uses Zend Framework 2 however may be ported to other frameworks quite easily. You are welcome to use it as the foundation for your next application.

The Starter Application includes:

  • A means of organising business logic (the Domain),
  • A flexible mechanism for database interaction (the Data Mapper),
  • A clean, clear API for client code (the Service Layer),
  • Factories for creating complex Domain objects,
  • An identity map to avoid duplicating Domain objects in memory,
  • Custom collections for Domain objects, and
  • Proxies for efficient loading of individual Domain objects and collections.

The Starter Application is aimed at intermediate-level developers who have ZF2 experience and are addressing problems which are too complex for transaction scripts or table modules. I am planning to maintain and improve both the code and this discussion, so please contribute any improvements by forking the code on github.

Overview

The Starter Application consists of two modules: Core and Mapper. All codes lives in Core, with the exception of the Data Mapper which is declared in Core and implemented in Mapper.

The presentation layer of the Starter Application (i.e. views and controllers), is straightforward. All the interesting work takes place in the model which includes the Domain, Data Mapper and Service Layer.

The MVC Model

Getting Started

To start building your own application you can:

  1. Install the Starter Application using the method described here
  2. Read each of the topics that follow, exploring the installed code as you go
  3. Follow the steps in Using the Starter Application