It’s been years since I first met the concept of Dependency Injection, and despite the influx of proposals, actual libraries, and spotty writing on the topic, it remains elusive in PHP. Well, with my newfound freedom to bang on my keyboard and commit real time to development and writing, I’m making this provocative and sometimes accusatory entry to perhaps spark some progress.
What is Dependency Injection?
Explaining Dependency Injection is not that hard:
[geshi lang=php]class KlingonBOP {
public $power = null;
public function __construct() {
$this->power = new WarpCore();
}
public function toWarp($factor) {
if($this->power->has($factor*10)) {
return true;
}
return false
}
}[/geshi]
This KlingonBOP class can be thought of as a client which depends on services (other objects) in order to function (the analogy is really to assist understanding the relationships), like the WarpCore object which is a service to the KlingonBOP client.
Now consider how write a unit test for this class. Unit testing works by testing classes in isolation from their dependencies – this rules out any unintended interference from other classes (i.e. a cascading set of failures), and also removes the need to actually develop the service/dependent class right now.
In the code above, we have an obvious problem – in a test, we cannot replace the WarpCore object since it is explicitly instantiated in the constructor. By replace, I mean write the tests using a Mock Object or Stub instead of a real WarpCore instance, something which has predictable constant return values.
Dependency Injection Strategies
Dependency Injection is all about how to inject these service objects into the client objects, making use of them in such a way they can be easily swapped by any other source code (not just tests!). Assuming you’re with me so far, you can identify the two most obvious Dependency Injection strategies:
1. Pass the service to the constructor as a parameter (contructor injection).
2. Pass the service to a class mutator/setter method (setter injection).
Using either of these, we can write a nice isolated unit test such as:
[geshi lang=php]require ‘Mockery/Framework.php’;
class KlingonShipTest extends PHPUnit_Framework_TestCase {
public function testAllowsWarpIfSufficientPowerAvailable() {
$ship = new KlingonBOP(
mockery(‘WarpCore_Stub’, array(‘toWarp’, ‘true’))
);
$this->assertTrue($ship->toWarp(1));
}
}[/geshi]
or
[geshi lang=php]require ‘Mockery/Framework.php’;
class KlingonShipTest extends PHPUnit_Framework_TestCase {
public function testAllowsWarpIfSufficientPowerAvailable() {
$ship = new KlingonBOP();
$ship->setWarpCore(
mockery(‘WarpCore_Stub’, array(‘toWarp’, ‘true’))
);
$this->assertTrue($ship->toWarp(1));
}
}[/geshi]
Now we can edit the class for the constructor injection or setter injection strategies as appropriate…
These two DI options are often called “Manual Dependency Injection” or “Construction By Hand”, a reference to the fact we are manually constructing and handing services to client objects. Every single service object gets the manual treatment, and that’s the norm in most PHP source code. It’s a primary underpinning of the concept of composition in OOP afterall.
So what’s the problem? It’s manual!
Anything manual has two facets. First, you need to know how to do it by hand (RTFM ). Secondly, you better do it right and add more tests. Third, it must be duplicated everywhere so you better not forget how to do it…ever!
Being manual isn’t all bad though, most objects need one, two or at most three service objects. It’s messy in large doses but quite manageable. It’s when things become unmanageable that it becomes interesting because its at that point all those fluffy ideals like unit testing and decoupling are thrown out the window (I prefer to flush mine down the drain, but everyone’s a critic).
The most likely outcome of the manual approach is the duplication of all that object wiring. What happens if the API changes? You need to manually re-edit all those instantiations and API calls across the application(s). When they break BC for the next version? Manually edit…again. The other is requiring in-depth knowledge of every detail including how internal objects are utilised. Besides adding additional onus on people to refer constantly to the manual, or far worse – the source code itself, it ignores the concept that only the outer public API has real value – the rest is just an annoying burden of knowledge weighing us down. I don’t know about other programmers, but I never bother to memorise anything but a library’s basics. Why should I? Programmers are supposed to be supported in their group laziness .
Let’s emphasise everything so far by moving onto Dependency Injection strategy three – the Factory and Abstract Factory design patterns. You didn’t really think I forgot about them, did you? .
Both patterns have simple goals, to centralise the creation logic of objects so they are easier to reuse, and reduce code duplication across your source code where the created objects are needed. Typically they wind up being class methods. Annoying, irksome, untestable, static class methods.
But let’s not go overboard, we can make the Factories into specific classes (they have a separate role afterall!) with public methods instead (PHP is obsessed with those bloody static methods so the trail ends there usually) which instantly makes them amenable to Dependency Injection strategies 1 and 2 (and allows you to mock/stub the factory object in tests). This does wonders, and then you find out about the punchline…
Factories add three layers of complexity, not two, so you now need to double up on Mock Objects and stubs in tests. To utilise Mock Objects in place of the dependent object (created by the Factory class for the dependee), you need a way to force the Factory to send out mocks or stubs in place of creating an actual new object. At this point you start scratching your head, so you throw in a setter which you realise won’t work (Factories don’t remember what they create, they just return the end product immediately), and perhaps toe across the line and apply the Registry pattern to make Factories hold a static memory of any mock/stub you need it to use instead of creating a new one. Sometimes you’ll simply do away with the Factory (basically accepting the cost of its loss in added complexity to the end user!) and fall back on setter/constructor injection.
We’ll assume that the tactic used is the simpler option – stub the Factory class, and set a canned response which is actually another mock/stub of the object being created. Twice the mocks, unless it’s a static method (yes, I am hating static methods way too often in this post ).
If there is such a thing a Singletonitis, Factoryitis is a close relative. A simple Factory is great, but then you realise the Factory isn’t creating one single universal object, it’s dynamically creating variations based on passed parameters, config files, or via multiple public methods for hand coded variations. That’s a lot of surrounding code, and if your original duplication varied on the inputs, a Factory buys you little benefit apart from centralising all that muck in one place where it builds up and grows into complex monolithic code blocks. And how do you even test the Factory anyway? You already tested each object it creates, so you duplicate that to maintain some sanity?
Note: You can test even static factories, once you remember to clean them up in reverse. Technically though, you still can’t mock or stub the buggers without adding test wiring – extraneous methods and properties only used by tests.
This raises a spectre of a problem – if a factory is not easily tested, and we sacrifice it to fall back on more testable injection strategies, we’re lost the benefits of the factory in reducing complexity to the end user and code duplication across the source. Can we take back control?
Dependency Injection: The Inversion Of Control
So we know the basics, inherently, without really calling them Dependency Injection outright, but Dependency Injection is also its own strategy by combining facets of all these strategies into a single comprehensive solution. Something that makes testing, composition, and configured creation easy without all the nasty consequences (well, so it claims – judge for yourself). From now on I’ll refer to this overarching strategy simply as Inversion Of Control (IOC). Often IOC is used interchangeably with DI which can be confusing, so bear in mind DI is IOC plus the original injection strategies I’ve mentioned so far.
IOC makes a few assumptions I’m already pushed around. It needs to make testing easy. It needs to avoid manual coding of dependencies. It also needs to remain ignorant of the dependencies until it’s told what to do. It also assumes all objects used in object creation remain loosely coupled (one of the cardinal sins often used in opposition to DI) and abide to API contracts (code to the interface, not the implementation!). In fact it presumes a common deferred approach – objects expect their dependents to be handed to them, but they never actively seek those dependencies by themselves. A sort of don’t call me, I’ll call you approach on the part of dependents.
The primary member of any IOC strategy is the Injector. You might see DI frameworks referring to the Container more often, but we’ll get around to why its called a Container – in there somewhere is always an Injector. As the name suggests, Injectors exist to inject dependents into dependees. Much as you do manually in the previous DI strategies.
Anyone familiar with objects will realise its role quite easily – the Injector creates all service objects with all of their own dependencies injected (preferably without creating an infinite loop though ) and injects those service objects into the client object being created before returning the client object for use. We’ll explore the concept and approach in another post soon, assess the current state of play in PHP of Dependency Injection, and see where DI can be dragged into actual source code in a useful way.
Until next time…
reference:http://blog.astrumfutura.com/2009/03/the-case-for-dependency-injection-part-1/
沒有留言:
張貼留言