OO Design

The Data Mapper

Data Mappers add to the development effort but they give the model the "freedom to be whatever it needs to be". A typical mapper includes:

  • A find($id) method that returns a single Domain object. The method checks the identity map to ensure the object is not already in memory before taking a trip to the database.
  • Other single-object finders, such as findByName($name).
  • Numerous collection finders, such as findAll($status = null) and findByFoo($id).
  • An insert($object) method which constructs an array from the object’s properties then constructs and executes an SQL insert statement.
  • An update($object) method which constructs an array from the object’s updatable properties then constructs and executes an SQL update statement.

In a simple case there may be one Data Mapper for each Domain object however this won’t always be the case. Martin Fowler’s POEAA has a good discussion on the variations, (Chapter 12, Object-Relational Structural Patterns). Patterns that don’t necessarily conform to the 1-to-1 norm include dependent mapping, embedded value, and each of the table inheritance mappers.

As a general rule I start by assuming a 1-to-1 relationship, and when this becomes strained I start looking at the alternatives. Even in cases where it seems obvious to break the 1-to-1 norm, such as class table inheritance, it may transpire that the 1-to-1 works better.

Here is a Bar mapper example.

namespace Mapper;

use Core;

class Bar extends AbstractMapper implements Core\iMapper\Bar
{
    public function find($id)
    {
        if ($exists = $this->getWatcher()->exists($this->getShortType($this), $id)) {
            return $exists;
        }

        $sql    = "SELECT * FROM bar WHERE id = ?";
        $stmt   = $this->getAdapter()->createStatement($sql);
        $result = $stmt->execute(array($id));

        if (!count($result)) {
            return null;
        }

        return $this->create($result->current());
    }

    public function findByFoo($id)
    {
        $sql    = 'SELECT * FROM bar WHERE foo = ?';
        $stmt   = $this->getAdapter()->createStatement($sql);
        $result = $stmt->execute(array($id));
        $rows   = $result->getResource()->fetchAll();

        return new Core\Domain\Collection\Bar(
            $this->getFactory(), $this->convertKeysToCamel($rows));
    }

    public function insert(Core\Domain\iDomain\Bar $bar)
    {
        $data = array(
            ':startDate' => $bar->startDate->format('U'),
            ':condition' => $bar->condition,
            ':foo'       => $bar->foo->id,
            ':baz'       => $bar->baz->id,
        );

        $sql    = 'INSERT INTO bar (startDate, condition, foo, baz) ';
        $sql   .= 'VALUES (:startDate, :condition, :foo, :baz)';
        $stmt   = $this->getAdapter()->createStatement($sql);

        $stmt->execute($data);
        $bar->id = $this->getAdapter()->driver->getLastGeneratedValue();
        $this->getWatcher()->add($bar);

        return $bar->id;
    }

    public function update(Core\Domain\iDomain\Bar $bar)
    {
        $data = array(
            ':id'        => $bar->id,
            ':condition' => $bar->condition,
            ':baz'       => $bar->baz->id,
        );

        $sql    = 'UPDATE bar SET condition = :condition, baz = :baz WHERE id = :id';
        $stmt   = $this->getAdapter()->createStatement($sql);
        $stmt->execute($data);
    }

}

Data Mappers are loaded via an abstract factory, the MapperLoader. Note that the mapper converts field names between under_score format in the RDBMS and camelCase for objects.

The Service Layer

My Service Layer provides whatever public methods are required by the controllers and occasionally views. To maximise the utility of the Service Layer, each of its classes is service-locator-aware, and each has its own event listener.

Core\Service\ServiceLoader is the abstract factory responsible for creating Service Layer services and you can see there how it creates and injects the event listener.

Here is the code for a sample Bar Service.

namespace Core\Service;

class Bar extends AbstractService
{
    public function updateBaz($bar, $baz)
    {
        $bar = $this->serviceLocator->get('Mapper\Bar')->find($bar);
        $baz = $this->serviceLocator->get('Mapper\Baz')->find($baz);

        $bar->baz = $baz;
        $this->serviceLocator->get('Mapper\Bar')->update($bar);

        // optional: trigger an event
        $this->eventManager->trigger(__FUNCTION__ . '.post', $this, array(
            'bar' => $bar,
            'baz' => $baz
        ));

        return $baz;
    }
}

As a general rule, all calls to Mappers to persist changes are made from the Service Layer. If this becomes difficult to manage, you might consider extending the Domain Watcher, and using the Unit of Work patten for persistence.

Service Layer Events

Service Layer Events can be used to separate out entire aspects of the application, such as logging or authentication, from the main workflow. Alternatively they may be used for one-off tasks such as triggering an email.

Events which are triggered by a range of workflows are best placed in the AbstractListener base class. Those that apply only to a particular Service Layer service may be placed in the concrete listener that belongs to that particular service. All listeners are stored in the Core\Service\Event namespace.

Here's an example of a listener that should be activated when Bar::updateBaz has executed.

namespace Core\Service\Event;

use Zend\EventManager\EventInterface;
use Zend\EventManager\EventManagerInterface;

class BarService extends AbstractListener 
{
    public function attach(EventManagerInterface $em)
    {
        $em->attach('updateBaz.post', array($this, 'emailAdmin'));
    }

    public function emailAdmin(EventInterface $e)
    {
        // do some stuff here
    }
}

Identity Map

I find it liberating to have an identity map in place. It is a kind of safety net that allows me to navigate the object graph, retrieve objects and make updates, safe in the knowledge that I am not compromising performance or model integrity.

Factories are the main users of the identity map. Before converting raw data in to an object, they check the data for the existence of an object identifier. If an identifier is present they use DomainWatcher::exists to see if a corresponding object has been loaded already. If there is no such object, the factory goes ahead and creates one, then adds it to the map using DomainWatcher::add.

Mappers are the second most common users of the identity map. Before executing a 'find' for a single object they consult DomainWatcher::exists to see if the object exists already. This avoids unnecessary calls to the database. Secondly, a newly created entity is usually given its identifier at the point where a Mapper inserts it in to the database. In this case, the Mapper adds the new entity to the identity map using DomainWatcher::add.

The domain watcher is a singleton. To avoid cross-contamination between tests, such as unit tests, call the reset method between tests. Other typical problems of singletons, such as unwanted dependencies, can be avoided by accessing the watcher via the service locator, rather than static method calls.

Tags: 

Collections

Collection classes are iterable and countable. The raw data for each member is stored in an array, and members are instantiated on request only. There is generally a collection class for each Domain class and it may include custom logic that applies only to a collection of the given objects.

Custom logic may relate to sorting, filtering, adding members, removing members, comparing collections, and more. For methods that filter or sort the collection, I find it easiest and safest to pass out a read-only clone, rather than reorder the underlying data.

Collections are commonly returned by a mapper. On construction a collection takes two parameters: a factory for creating objects of the given type, and an array such as a database record set, where each element is a sub-array with the raw data for a single object. The code below is for a Foo collection. Notice that most of the functionality is implemented in the AbstractCollection.

namespace Core\Domain\Collection;

use Core\Domain\Collection\iCollection;
use Core\Domain\iDomain;

class Foo extends AbstractCollection implements iCollection\Foo 
{
    public function add(iDomain\Foo $foo)
    {
        $this->doAdd($foo);
    }
} 

The role of the iCollection\Foo interface is to ensure that only iDomain\Foo objects are added to the collection. In addition, it makes way for collection substitutes such as collection proxies. Following is an interface for a Foo collection.

namespace Core\Domain\Collection\iCollection;

use Core\Domain\iDomain;

interface Foo extends Collection
{
    public function add(iDomain\Foo $foo);
}

Collection Proxies

In the same way that there are proxies for Domain objects, there are proxies for collections of Domain objects too. The following class diagram shows how concrete collections (e.g. Core\Domain\Collection\Foo) relate to their proxies (Core\Domain\Collection\Proxy\Foo).


Class Diagram: Collections and their Proxies

Both a collection and its proxy inherit most of their characteristics through a common hierarchy of interfaces. The key difference is that when the collection extends its parent it becomes an \Iterator, and when the proxy extends its parent it becomes an \IteratorAggregate. The 'iterator aggregate' is a proxy for a real iterator, the actual collection.

From the constructor of the AbstractCollectionProxy you can see that proxies are injected with:

  • The Mapper that must be used to retrieve the collection,
  • The name of the method to call on the Mapper, and
  • An array of arguments that must be provided to the method.

When client code attempts to iterate the proxy or access collection methods, the proxy is realised and calls are passed to the true collection. Any attempts to access custom logic methods are passed from the proxy to the real collection using method call overloading.

Here is the code for a Foo collection proxy:

namespace Core\Domain\Collection\Proxy;

use Core\Domain\Collection\iCollection;
use Core\Domain\iDomain;

class Foo extends AbstractCollectionProxy implements iCollection\Foo
{
    public function add(iDomain\Foo $foo)
    {
        $this->getIterator()->add($foo);
    }
}

Collection proxies are used extensively by factories as a proxy for multi-valued properties. Under normal operation, it is not possible for the factory to know which multi-valued properties will be accessed on the object under construction. Instead of retrieving and attaching a large number of properties which may not be used, the factory provides a collection proxy which can efficiently retrieve the collection if/when required.

Factories

Domain object factories are some of the longest and most complex classes. They are loaded via an abstract factory, Core\Domain\Factory\FactoryLoader, and use the factory method pattern to execute the same series of steps irrespective of which particular factory is in use.

The steps outlined in the table below work well with my way of thinking. You should add/remove methods to come up with an implementation that works best for you and incorporate it in to the abstract base factory. Concrete objects should override base methods as required.

Note that when creating an object the code path differs depending on whether the object is an existing entity or entirely new. To differentiate between existing and new entities prior to instantiation, factories test the input data for the existence of an identifier. To differentiate after instantiation, factories test the object for an identifier.

Step Actions
New Object Defaults

Default property values for new entities are set in doNewObjectDefaults. Defaults may override a passed in value, or may apply only when no value is passed. In the case of multi-valued properties, if no defaults apply I usually add an empty collection of the given type.

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    protected function doNewObjectDefaults(array &$data)
    {
        $data['startDate'] = empty($data['startDate']) ? new \DateTime() : 
                $data['startDate'];
        $data['condition'] = 'Good';
        $data['bats']      = $this->getCollection('Bat');
    }
}
Type Conversion

Property values passed to the factory are either scalars, objects or arrays. Exactly how they are handled by the factory depends entirely on the context. Here are some common conversions:

  • A string that represents a date may be converted to a unix timestamp or a \DateTime object
  • An array may be converted to a new Domain object
  • An array of arrays may be converted to a new Domain object collection
  • An integer or other identifier may be converted to a Domain object proxy

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    // ... //

    protected function doTypeConversion(array &$data)
    {
        if (is_int($data['startDate']) || ctype_digit($data['startDate'])) {
            $data['startDate'] = new \DateTime('@'.$data['startDate']);
        }

        if (is_int($data['foo']) || ctype_digit($data['foo'])) {
            $data['foo'] = $this->getProxy('Foo', $data['foo']);
        } elseif (is_array($data['foo'])) {
            $data['foo'] = $this->serviceLocator->get('Core\Domain\Factory\Foo')
                ->create($data['foo']);
        }

        if (is_int($data['baz']) || ctype_digit($data['baz'])) {
            $data['baz'] = $this->getProxy('Baz', $data['baz']);
        } elseif (is_array($data['baz'])) {
            $data['baz'] = $this->serviceLocator->get('Core\Model\Factory\Baz')
                ->create($data['baz']);
        }

        if (isset($data['bats']) && is_array($data['bats'])) {
            $data['bats'] = $this->getCollection('Bat', $data['bats']);
        }
    }

    // ... ..
}
Add Relations

In the case of multi-valued properties it is common for no value to be present in the factory input data. In such cases the doAddRelations method adds a collection proxy, which is ready to retrieve the related objects on demand.

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    // ... //

    protected function doAddRelations(array &$data)
    {
        if (empty($data['bats'])) {
            $data['bats'] = $this->getCollectionProxy(
                'Bat', 'findByBar', array($data['id']));
        }
    }

    // ... //
}
Instantiation

Domain objects are instantiated with an array of property values and optional arrays of finders and factories. doInstantiation uses the service manager to retrieve any required mappers and factories, then instantiates the Domain object.

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    // ... //

    protected function doInstantiation(array $data)
    {
        return new Domain\Bar(
            $data,
            array('Bat' => $this->serviceLocator->get('Core\Domain\Factory\Bat')),
            array('Bat' => $this->serviceLocator->get('Mapper\Bat')));
    }

    // ... //
}
Post Initialisation

This is a good place to check preconditions for objects entering the active state. The tests often involve high-level business logic and may need to crawl the object’s dependencies. Code paths often differ between new and existing entities. Illegal conditions result in an exception.

Example:

namespace Core\Domain\Factory;

use Core\Domain;
use Core\Exception;

class Bar extends AbstractFactory
{
    // ... //

    protected function doPostInit(Domain\AbstractDomain $bar)
    {
        if ($bar->baz->status !== 'Active') {
            throw new Exception\Factory('$bar->baz must be Active');
        }
    }
}
Domain Watcher

doDomainWatcher checks that an object of the given type and identity is not already in play. If it is, the existing object is returned. Otherwise the new object is added to the identity map and returned.

This method is final and can not be overwritten.

Domain object factories are service-locator-aware and are therefore capable of many things. In the past I have added methods to configure a Domain event manager, for example. Care is required however to avoid introducing unnecessary dependencies. It would not make sense to use Service Layer services within a factory.

Note that the AbstractFactory includes three methods and a property which are related to the 'flavour' of the factory. Flavour is discussed as part of the N+1 Selects Problem.

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.