Add a text that gives some background on dependency injection and provides a few...
authorGunnar Wrobel <p@rdus.de>
Fri, 2 Oct 2009 13:41:12 +0000 (15:41 +0200)
committerGunnar Wrobel <p@rdus.de>
Fri, 2 Oct 2009 13:41:12 +0000 (15:41 +0200)
framework/Injector/doc/Horde/Injector/usage.txt [new file with mode: 0644]

diff --git a/framework/Injector/doc/Horde/Injector/usage.txt b/framework/Injector/doc/Horde/Injector/usage.txt
new file mode 100644 (file)
index 0000000..19c9be1
--- /dev/null
@@ -0,0 +1,390 @@
+Introduction
+============
+
+The dependency injection pattern
+(http://en.wikipedia.org/wiki/Dependency_injection) is a useful
+approach that can help to avoid using global variables or state. If a
+class depends on a connection to a database then this connection is
+often pulled into the class using a singleton pattern or by using a
+global variable.
+
+Instead of providing the class with knowledge about the global state
+it is often preferable to "inject" the dependency into the class from
+the outside. This usually happens within the class constructor. To get
+hold of a database connection a class constructor could for example
+require an object that implements a database interface instead of
+using a singleton pattern.
+
+This way the dependencies of a class are immediately visible in the
+constructor. It is not necessary to search the code of the class for
+references to the global scope. This usually also helps to decouple
+dependencies between different code modules. Another major benefit of
+dependency injection is the fact that it facilitates unit testing of
+complex systems.
+
+Horde_Injector
+==============
+
+Horde_Injector provides a "Dependency Injection" framework.  For PHP
+there exist several dependency injection frameworks
+(e.g. http://stubbles.net,
+http://components.symfony-project.org/dependency-injection) with
+extensive feature lists. So there is hardly any need for another
+framework with similar capabilities.
+
+The essential part of dependency injection is the structure of classes
+with dependencies. They need to be amenable for an external management
+of their dependencies. If that is the case for a given class then most
+dependency injection frameworks should have no problem handling this
+class within the framework. The choice of the actual framework should
+not matter anymore.
+
+Horde_Injector provides only a minimal version of dependency
+injection.  It is somewhere in between the frameworks mentioned above
+and Twittee (http://twittee.org/). The primary goal is to drive
+refactoring of classes with complex dependencies so that their
+dependencies can be uncoupled and they can be used with a dependency
+injection framework.
+
+Making classes amenable to dependency injection
+===============================================
+
+As trivial as it may sound: a class can be managed by a dependency
+injection framework if the class allows the framework to inject its
+dependencies from the outside. That means that the class may *NOT*
+
+ - pull in a dependency using global state via the singleton
+   pattern:
+
+    External_Class::singleton())
+
+ - create new objects with dependencies:
+
+    $db = new DB();
+    $b = new User($db);
+
+ - use global variables:
+
+    global $conf;
+
+    $db = new DB($conf['sql');
+
+In most cases the class should receive dependencies and required
+parameters within the constructor.
+
+Using Horde_Injector
+====================
+
+The Horde_Injector class is a simple container that allows you to fill
+it with a number of elements that can be retrieved later:
+
+ $a = new Horde_Injector(new Horde_Injector_TopLevel());
+ $a->setInstance('a', 'a');
+ echo $a->getInstance('a');
+
+ string(1) "a"
+
+Here we assigned a concrete instance to the injector. In fact not even
+an instance but a simple type: a string. Usually you would register an
+object.
+
+But there might be situations - and in fact these are what dependency
+injection is about - where you do not want to register a concrete
+instance. You might not already have all the dependencies for creating
+an instance in place. So all you want to do is to register the
+required build instruction for generating an instance.
+
+This is something that you can do by registering a wrapper object that
+implements the Horde_Injector_Binder interface. This wrapper object
+needs to be capable of creating the concrete instance:
+
+ class Binder implements Horde_Injector_Binder
+ {
+     public function create(Horde_Injector $injector)
+     {
+         return 'constructed';
+     }
+     public function equals(Horde_Injector_Binder $binder)
+     {
+         return false;
+     }
+ }
+
+ $a = new Horde_Injector(new Horde_Injector_TopLevel());
+ $a->addBinder('constructed', new Binder());
+ var_dump($a->getInstance('constructed'));
+
+ string(11) "constructed"
+
+The example above demonstrates this approach by using the dummy Binder
+class which implements Horde_Injector_Binder. Once
+getInstance('constructed') is called on the injector object it will
+determine that there is no concrete instance for 'constructed' yet. It
+then looks for any binders that might be capable of creating
+'constructed' and calls the create() function of such a binder.
+
+Here the binder is simple again and does not even return an object but
+a simple string. It also makes no use of the Horde_Injector instance
+delivered as argument to the create() function. Usually the provided
+injector will be used to retrieve any missing dependencies for the
+instance to be created.
+
+Default Binders
+===============
+
+Horde_Injector comes with two default Binder implementations so that
+you don't have to define you own binders.
+
+Lets look at the factory binder first:
+
+ class Greet
+ {
+     public function __construct($somebody)
+     {
+         $this->somebody = $somebody;
+     }
+     public function greet()
+     {
+         print 'Hello ' . $this->somebody;
+     }
+ }
+ class Factory
+ {
+     static public function getGreeter(Horde_Injector $injector)
+     {
+         return new Greet($injector->getInstance('Person'));
+     }
+ }
+ $a = new Horde_Injector(new Horde_Injector_TopLevel());
+ $a->setInstance('Person', 'Bob');
+ $a->bindFactory('Greet', 'Factory', 'getGreeter');
+ $a->getInstance('Greet')->greet();
+
+ Hello Bob
+
+This time the Factory in the example above really pulls a dependency:
+A person. We explicitly registered the string "Bob" with the injector
+and associated it with the interface name "Person".
+
+The Horde_Injector_Binder_Factory binder can be registered with the
+injector using the "bindFactory()" shortcut. It takes the interface
+name (here it is "Greet") and requires a class and a method name. This
+is assume to be the factory creating the concrete instance.
+
+Once getInstance('Greet') gets called the injector refers to the
+binder (as no concrete instance has been created yet). The binder
+delegates to the factory to actually create the object.
+
+The whole thing is also possible with a little bit more magic. The
+second approach implemented by Horde_Injector_Binder_Implementation
+requires type hinting to work:
+
+ interface Person
+ {
+     public function __toString();
+ }
+
+ class World implements Person
+ {
+     public function __toString()
+     {
+         return 'World';
+     }
+ }
+
+ interface Greeter
+ {
+     public function greet();
+ }
+
+ class Hello implements Greeter
+ {
+     public function __construct(Person $somebody)
+     {
+         $this->somebody = $somebody;
+     }
+     public function greet()
+     {
+         print 'Hello ' . $this->somebody;
+     }
+ }
+ $a = new Horde_Injector(new Horde_Injector_TopLevel());
+ $a->bindImplementation('Person', 'World');
+ $a->bindImplementation('Greeter', 'Hello');
+ $a->getInstance('Greeter')->greet();
+
+ Hello World
+
+The crucial part here is that the "Hello" class indicates in its
+constructor that it requires an object implementing the interface
+"Person". Horde_Injector is capable of detecting this via
+reflection. It will automatically search for the dependency and try
+to create an instance implementing this interface.
+
+In order for this to work we bind two classes to two interfaces:
+"World" to "Person" and "Hello" to "Greeter". Once the injector tries
+to create the "Greeter"-instance it will be able to fetch the required
+"Person" dependency by creating a "World" object.
+
+In case you remember that printing the little resulting string can be
+slightly easier while even using far less code: Dependency injection
+is meant for complex situations.
+
+Nevertheless the example hopefully demonstrates how to handle
+different implementation options using dependency injection: You may
+have different drivers that all fulfill a given interface. The
+Horde_Injector gives you an easy method to define which drivers you
+actually want to use without actually instantiating them. The concrete
+setup will only be build once you really need a concrete instance.
+
+Preparing a class for Horde_Injector
+====================================
+
+Assume you have the following simple class that represents a common
+structure found in many of the Horde packages:
+
+ class Horde_X
+ {
+     /**
+      * Instance object.
+      *
+      * @var Horde_X
+      */
+     static protected $_instance;
+     /**
+      * Pointer to a DB instance.
+      *
+      * @var DB
+      */
+     protected $_db;
+     /**
+      * Attempts to return a reference to a concrete Horde_X instance.
+      *
+      * @return Horde_X  The concrete Horde_X reference.
+      * @throws Horde_Exception
+      */
+     static public function singleton()
+     {
+         if (!isset(self::$_instance)) {
+             self::$_instance = new Horde_X();
+         }
+         return self::$_instance;
+     }
+     /**
+      * Constructor.
+      */
+     public function __construct()
+     {
+         global $conf;
+         $this->_db = DB::connect($conf['sql']);
+     }
+ }
+
+The class obviously depends on a database connection. The constructor
+above does not allow for dependency injection as it constructs the
+database connection itself. It uses the global variable $conf in order
+to get the settings for this connection. A constructor allowing
+dependency injection would look like this:
+
+     /**
+      * Constructor.
+      *
+      * @param DB $db A database connection.
+      */
+     public function __construct(DB $db)
+     {
+         $this->_db = $db;
+     }
+
+Of course this connection must be provided from somewhere. The
+application using Horde_X might simply provide it when creating the
+Horde_X instance. If the application is however using a dependency
+injection framework then this framework would be required to provide
+the required database connection.
+
+Getting rid of singletons?
+==========================
+
+From the viewpoint of dependency injection Horde_X can be used now as
+it allows external injection of its dependencies. We could throw away
+the singleton now. However there might be some reasons why we would
+like to keep the singleton() method. One of the reasons might be
+backward compatibility as some other classes or applications are bound
+to use the method. Another reason might be that we want to clarify how
+to get a functional instance of the class to somebody just looking at
+the Horde_X class.
+
+We could keep the following singleton method:
+
+     static public function singleton()
+     {
+         if (!isset(self::$_instance)) {
+             global $conf;
+             $db = DB::connect($conf['sql']);
+             self::$_instance = Horde_X($db);
+         }
+         return self::$_instance;
+     }
+
+
+Result
+======
+
+The final result that can be used with a dependency injection
+framework and still provides a backward compatible singleton method:
+
+ class Horde_X
+ {
+     /**
+      * Instance object.
+      *
+      * @var Horde_X
+      */
+     static protected $_instance;
+     /**
+      * Pointer to a DB instance.
+      *
+      * @var DB
+      */
+     protected $_db;
+     /**
+      * Attempts to return a reference to a concrete Horde_X instance.
+      *
+      * @return Horde_X  The concrete Horde_X reference.
+      */
+     static public function singleton()
+     {
+         if (!isset(self::$_instance)) {
+             global $conf;
+             $db = DB::connect($conf['sql']);
+             self::$_instance = Horde_X($db);
+         }
+         return self::$_instance;
+     }
+     /**
+      * Constructor.
+      *
+      * @param DB $db A database connection.
+      */
+     public function __construct(DB $db)
+     {
+         $this->_db = $db;
+     }
+ }