Saturday, November 14, 2009

How to eliminate singletons (part 2)

In the previous post I hacked-up a very small component for automating dependency injection of php classes, which mimic the behavior of the moltitude of dependency injection frameworks out there.
The question arised in the last part of the article was How can I construct objects with a shorter lifetime than the application-wide one? In Zend Framework's case there are many controllers and view helpers that we want to instantiate only if necessary and which proper instantiation should happen only after a bit of logic has been executed, for instance after the http request has been elaborated by the router to produce a controller name.

With manual dependency injection the solution would be straightforward: just inject a ControllerFactory in the Zend_Controller_Dispatcher_Standard, which is the object that currently creates the controller. But the Zend_Controller component manages userland controllers and we cannot code a factory in advance to cover the possible use cases in every domain, nor we want the end-user to write boilerplate code in the form of a factory for his controllers.
Since the only viable solution is automatic dependency injection, we should create a configurable factory instead:
$controllersConfig = array(
    'My_Controller' => array(
        'myClass' => 'My_Class'
    ),
    'My_Class' => array(
        // ...My_Class's collaborator names listed by key
    )
);
$frameworkConfig = array(
    'Zend_Controller_Dispatcher_Standard' => array(
        'controllerProvider' => 'Zend_Controller_Provider' 
    ),
    'Zend_Controller_Provider' => array(
        'config' => new Zend_Config($controllerConfig) 
    )
    // ...collaborator configuration of the router, the front controller, etc.
);
I use the name Provider since it was popularized by Guice, but it is in fact a configurable Abstract Factory. The Zend_Controller_Provider class can be decoupled with a small interface:
interface  Zend_Controller_Provider_Interface
{
    public function getController($name);
}
class Zend_Controller_Provider extends Injector 
      implements Zend_Controller_Provider_Interface
{
    public function __construct($options)
    {
        parent::__construct($options['config']);
    } 
 
    public function getController($name) 
    {
        return $this->newInstance($name);
    }
}
The provider subclasses the Injector from the previous example to keep the code example short, but using composition of an Injector instance would make no difference.
The Injector code has to be extended a little to allow specifying objects in the configuration:
<?php
class Injector
{
    // ... constructor and private members
    public function newInstance($class)
    {
        if (is_object($class)) {
            return $class;
        }
        // default flow
        if (!isset($this->_config[$class])) {
            // it's a literal value like 'mydbpassword';
            return $class;
        }
        $collaborators = array();
        foreach ($this->_config[$class] as $collaboratorName => $collaboratorClass) {
            $collaborators[$collaboratorName] = $this->newInstance($collaboratorClass);
        }
        return new $class($collaborators);
    }
}
Including objects in the configuration should be done only in the case we need collaborators which are really only Value Objects with no behavior, like Zend_Config. It would be more complicated to set up different Zend_Config objects for injection, while it is actually a newable class (and so should not be injected, like we would not inject an ArrayObject).

Let's list the advantages we have just gained:
  • Independent instantiation of controllers. The Dispatcher will new only the controller actually needed (but it is the injector that will call new). Another provider can be set up for view helpers and other short-lived classes.
  • Real unit testing for controllers and view helpers: it will be easy to inject stubs and mocks in a controller since now it is forced to have setters or a unified constructor.
  • Real unit testing for the dispatcher: we can inject easily a fake Zend_Controller_Provider_Interface implementation, and test that given the right parameters it requests the chosen controller class.
Note that it is correct for shorter-lived classes to have field references to longer-lived ones. For instance the Url view helper should be injected with the current instance of the router since it needs the collaboration of something that knows all the defined routes. So one more question arises: how the problem can be solved now, given that the Zend_Controller_Provider class is created only with a Zend_Config as a parameter?
The simplest thing that can work in this case is to implement a generic Provider interface with a setInjector(Injector $longLiveInjector) method, so that when the interface is detected the original injector can create a clone of itself (that still references the created objects) and pass it to the short-lived objects provider, in this example Zend_Controller_Provider. The Provider then can add to its Injector the controller configuration instead of extending it, even favoring composition over inheritance.

I'm sure production-ready DI frameworks solve all these problems and maybe other ones, since it took us only two days to figure out the theory of operations. Automated Dependency Injection is a must-have in Zend Framework 2.0 and this is a proof of concept of how it can be implemented.

4 comments:

  1. I've been using DI using the Symfony DI container for some time now. It integrates smoothly into Zend_Application, transparently taking of the container.
    The biggest problem is that the current interface for controller actions has a constructor in the interface (which doesn't make sense for several reasons). Even if you replace the controller instantiating code in the Dispatcher with a ControllerFactory, you can't inject any meaningful, unless you're DI container can inject into already existing objects using setter injection. I think a lot can be learned by looking at the interfaces and moving pieces in ASP.NET MVCs implementation.

    ReplyDelete
  2. The singletons elimination process is scheduled for 2.0, so there are no problems with backward compatibility breaks. The constructor signature can be deleted if needed. :)

    ReplyDelete
  3. The Symfony DI Component is my favourite, too.

    Nice clean interface,
    unit tested,
    upcoming PHP 5.3 Version announced,
    well documented,
    easy to integrate, even in existing Zend based Projects.

    At the moment, i don´t see a reason to build up a custom DI container.

    Like Fabien Potencier said in on of his talks, use Zend Framework and Symfony Parts together in your projects.

    It really gives you benefits...

    ReplyDelete
  4. Not bad article, but I really miss that you didn't express your opinion, but ok you just have different approach

    ReplyDelete