The Recap
In Part 1 of this series, we briefly passed over the concept of Dependency Injection, identifying several injection strategies as below:
1. Constructor Injection
2. Setter Injection
3. Factory Design Pattern
These three patterns have been the mainstay in managing dependencies in PHP source code ever since PHP was capable of handling object oriented programming. However, these strategies can accumulate problems:
1. They require manual management of dependencies when creating new objects.
2. They require users to understand the internal workings of dependencies.
3. They clutter APIs with undocumented methods.
4. They add complexity to tests since manual injections must be performed.
5. They increase the cost of change (manual editing is needed for API changes)
6. They promote static methods in PHP (easier than creating specific objects).
This is not to say, don’t use them! In most circumstances, application of the well known strategies is not only possible but advisable. There is little benefit adding the complexity inherent in Inversion Of Control to simple scenarios.
Inversion Of Control applies to the complex, not the simple.
In isolation, none of these strategies are anything but innocent. But their combined complexity increases as your application grows and networks of classes acumulate. That’s why simple examples of Dependency Injection are doomed to failure – the problems have simple solutions. What we need is something to illustrate that Dependency Injection and Inversion Of Control are applicable to something more concrete, like a fully formed Klingon Bird Of Prey with all the bells and whistles!
[geshi lang=php]class Ship_Klingon_BirdOfPrey {
// arrays indicate multiple object instances are required
protected $warpCore = null;
protected $disruptorBanks = array();
protected $warpEngine = null;
protected $shields = array();
protected $crew = null;
protected $shipsLog = null;
protected $computer = null;
protected $sensors = array();
// …
}[/geshi]
Are you getting the picture yet? Consider any application based on a complicated process. You may have dozens of objects working in concert towards a single goal. Kapla!
To create the client object, a Bird Of Prey, you need to inject every service object it requires since they are needed. In a simple use case, however, the user only wants a damn ship! They don’t care about the components unless they have a need to customise some internal operation. In this scenario, the Factory option might work. Using the Factory, we need to cover off on the testing piping using static means. We could just use any available setters or constructor parameters – but then our tests are going to be huge whenever a new Ship is needed.
But let’s dig deeper! How will we use the Ships Log? Every component will need to report it’s status there – so the Log is a service to the Ship, and to every other sub-system. Let’s double our Factory method length. Now, what if the computer needs to liase with the Sensors, acquire a Target via the Computer, and order the Disruptor Banks to fire? So the Sensors, Log, Disruptor Banks and possible Crew complement become needed by the Computer. Let’s double…nay…quadruple everything.
Next we decide to give the Computer a database, along with the Log, the Ship and maybe the Crew. Or we realise half this stuff are Models anyway, so give them all databases to play with!
Excuse me while I sit in a corner and cry…I only wanted a bloody ship! Why are making me do all this?
The Service Locator Pattern
Complex problems can still have simple solutions, and the Service Locator has long superceded the Factory when the going gets a bit tougher. Manually handling complex dependencies is insane (on all levels), so a little configuration can be handy.
A simple Service Locator is used like a map. If an object needs a specific service object, it refers to the Locator to retrieve a copy. You can embellish the system in any number of ways using keywords, tagging, URLs, and even utilise concrete Factories as needed, but the simplest Service Locator is a plain old array (which is also the simplest Registry):
[geshi lang=php]$locator = array(
‘log’ => new Ship_Log,
‘warpCore’ => new Ship_WarpCore,
‘computer’ => new Ship_Computer,
‘warpEngine’ => new Ship_WarpEngine,
// …
);[/geshi]
If you pass this locator to the Bird Of Prey, and let the BOP pass it to all other service objects it retrieves from the Locator array, then you can cut out the complex web of dependencies from manual management and let them all refer to the Locator instead. You can even adapt the Locator in tests to return Mock Objects. Aside from object lookup, you can also integrate configuration so the located objects can be constructed correctly.
So where are the negatives? Well, now everything needs to know about this super Service Locator, since otherwise objects couldn’t find their dependencies. It also ignores the order of creation – some objects need to be created before others or the more circular dependencies will fail to be available. If we try to create the Computer, but not the Log, then we can’t give the Computer a Log and the locator will fail. This would necessitate…manual intervention. We’d need to manually track and switch around code across numerous classes to ensure the order of creation is executed correctly. This is far from ideal, so while a Service Locator is a highly useful tool in the toolbox, orchestrating the dependency web needs something more.
The Next Obvious Evolution
As we gradually evolve from simple injection, the Service Locator was revealed as a capable, lightweight, flexible solution. It’s primary problem was dependency resolution. Rather than scatter this across all those classes, why not simply centralise them in the Service Locator itself? It’s at this point we shift from a Locator, to a simple Dependency Injection Container (ah, finally the Container term ) and leave our poor overburdened objects free of this distraction to focus solely on their roles.
Here’s an ultra simple IOC container:
[geshi lang=php]class DIContainer {
protected $_data = array();
public function set($name, $value) {
$this->data[$name] = $value;
}
public function get($name) {
if (isset($this->data[$name])) {
return $this->data[$name];
}
return $this->call(‘get’.ucfirst($name));
}
protected function getLog() {
$log = new Ship_Log($this->get(‘logfile’));
$this->data['log'] = $log;
return $log;
}
protected function getWarpCore() {
$warpCore = new Ship_WarpCore();
$this->data['warpCore'] = $warpCore;
$warpCore->setLog($this->get(‘log’));
}
protected function call($method) {
return $this->{$method}();
}
// …
}[/geshi]
This new Container class is a really simple implementation of a DI Container which assumes all client objects will utilise the exact same service objects. Like a Service Locator, you can use it to grab registered objects. However, it is written to also determine the order of creation of any object requested, then create it, and finally return it.
[geshi lang=php]$container = new DIContainer;
$container->set(‘logfile’, ‘/tmp/shiplog’);
$warpCore = $container->get(‘warpCore’);[/geshi]
A completed version could utilise the simple Strategy Pattern so you could have a single container for specific Model types accepting configuration options for everything the Container needs.
[geshi lang=php]$container = new DIContainer_Ship;
$container->set(‘type’, ‘Klingon BOP’);
$container->set(‘logfile’, ‘/tmp/shiplog’);
$container->set(‘db_type’, ‘mysql’);
$container->set(‘ship_db_table’, ‘ships’);
$container->set(‘capacity’, ’300′);
$ship = $container->get(‘ship’);[/geshi]
Of course, let’s not let this get away free . The obvious flaw is the amount of manual programming it still needs, even though it’s drastically reduced the overall code need by avoiding code duplication and centralising this task. While the dependency management is cool, the wiring is all manual. Adding the element of automation to the new Container concept with a little Reflection and typehinting could go far. Adding cloning might speed up the creation of Containers by allowing minor configuration tweaks. Adding object replacement would allow for Mock/Stub insertion. I’m sure you can think of lots of added elements to make something easier to utilise.
Conclusion
We’ve finally met the simplest kind of IOC possible. In Part 3, we’ll move outside this simple and flawed implementation, and take a look at the forms of IOC frameworks living in the wild whether they be PHP, Python or Ruby. Frameworks should, in theory, make these Containers a lot easier to setup and maintain in a more strucured form that removes the need for customising classes. Instead mixing convention with some configuration, and taking advantage of Reflection, should produce more maintainable and testable options.
reference : http://blog.astrumfutura.com/2009/03/the-case-for-dependency-injection-part-2/
沒有留言:
張貼留言