From: Gunnar Wrobel Date: Fri, 2 Oct 2009 13:41:12 +0000 (+0200) Subject: Add a text that gives some background on dependency injection and provides a few... X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=513c1419ec8aec34b3eb10bd9f233536e6e6cad4;p=horde.git Add a text that gives some background on dependency injection and provides a few examples. --- diff --git a/framework/Injector/doc/Horde/Injector/usage.txt b/framework/Injector/doc/Horde/Injector/usage.txt new file mode 100644 index 000000000..19c9be1bf --- /dev/null +++ b/framework/Injector/doc/Horde/Injector/usage.txt @@ -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; + } + }