Tuesday, June 7, 2011

Dependency Injection: Service Locator, Constructor , Setters - what to choose?

Dependency Injection is a great thing, and this thing can be implemented in different ways:
  •  Injection through constructor - most clear and straight way for coding. 
But not always this way can be used. Object can't be passed in through constructor, when we need more than one instance of the object. Also, it's not good for performance to create objects, which are optional (used inside 'if' clauses). We can fix it by using factories, but not when we need only one instance of the optional object (because we will create instance of factory anyway). 
  • Service Locator (SL, or Service Container, or Objects Manager, or  Resources Manager). 
At first look, this pattern can create very tight coupling, because all objects, which will require Service Locator, will be dependent of existing of this object. For standalone modules it could look even as forbidden pattern. But we can use this pattern in smart way: object should not require whole SL class, but only necessary Interface.
  • Setters - most "dirty" way.
Little explanation of this short word: object have two methods for injection, getter and setter. Getter it's method to get instance of the needed object. Setter is method to set this instance. Why this way is dirty? Because, when we call method, we don't know dependencies of this method. If there is no comments for this method, we have to read source code of this method to know, objects of which classes are used in this method. So, we have to remember, that we need to set some setters before calling this method - that's why it is dirty way. But, we can create one little exception for the Setters method, to make it usable. We can declare mutual defended object in getter, so if instance was not set through the setter, instance of default class will be created and returned. Yes, it's not best way and we should avoid it when it possible, because it means using 'new' keyword. But we still able to set mock-object for testing.

So, my recommendations, how to choose variant of DI:
  1. If the instance of the object is not optional - require it in the constructor.
  2. If the instance is optional, require in the constructor the Interface with method for creating the instance.
  3. If you want to remove argument from constructor by some reasons,and if after doing this you will achieve some very good result, you can use Setter with Getter and default value.
PHP code to illustrate:

<?php

interface ISessionCreator
{
    public function getSession();
}

interface ICacheStorageCreator
{
    public function getCacheStorage();
}

/*
 * Each getter of Service Locator should be represented in separate interface
 * to avoid tight coupling
 */

class ServiceLocator implements ISessionCreator, ICacheStorageCreator
{
    protected $cache_storage;

    public function getSession()
    {
        return new Session();
    }

    public function getCacheStorage()
    {
        if (empty($this->cache_storage)) $this->cache_storage = new CacheStorage();
        return $this->cache_storage;
    }

    /*
    * We can implement changing of instances (for testing) in 2 ways:
    * by extending existing class, or by using setters.
    */

    public function setCacheStorage($storage)
    {
        $this->cache_storage = $storage;
    }

}

class Injections
{
    protected $logger;
    protected $user;
    protected $session;

    //Example #1
    public function __construct(User $user, ISessionCreator $session_creator)
    {
        $this->user = $user;
        //ISessionCreator is not optional - instance will be created without "if"s.
        $this->session = $session_creator->getSession();
    }

    //Example #2
    public function SaveIfMatch($check, ICacheStorageCreator $storage_creator)
    {
        if ($check) //that's why ICacheStorageCreator is optional here
        {
            $storage = $storage_creator->getCacheStorage();
            $storage->save(time());
        }
        else $this->getLogger()->write(false);
    }

    //Example #3
    public function setLogger(ILogger $logger)
    {
        $this->logger = $logger;
    }

    public function getLogger()
    {
        if (empty($this->logger)) $this->logger = new Logger();
        return $this->logger;
    }
}

$SL = new ServiceLocator();
//In constructor we need any object, which implements ISessionCreator interface
//it will be our Service Locator
$example = new Injections(new User(), $SL);
//In this method we need any object, which implements ICacheStorageCreator interface
//it will be our Service Locator too.
$example->SaveIfMatch(true, $SL);
//So, in both of this cases, we gave one object, but we could use any other objects,
//because Interface was used, so there is no tight coupling to SL-class here.

Useful links:
Dependency Injection
Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
Miško Hevery: Writing Testable Code

No comments:

Post a Comment