(Editor's note: be sure to see the post script for strong criticism against the example and the post.)
Here's how I think of DI. We all know this is bad:
class MyClass {
private HashMap myMap = new HashMap();
}
because we should program to the interface, yes? Like so:
The conventional wisdom was always:
class MyClass {
// more flexible, in theory
private Map myMap = new HashMap();
}
Program to the interface, since it will be easy to change the implementation.True that. But then the implicit kicker was:
And when you are done with your application and have been given a couple of weeks by management to tweak performance while wearing your PJ's and drinking a fine Chardonnay, you can go through the code and select the implementation that's right for you.Whoa, Nelly! *throws RealityCheckException*. When would that be, again?
What if we tried this:
class MyClass {
private Map myMap;
public MyClass( ... , Map myMap ) {
this.myMap = myMap;
}
}
The beauty of this, IMHO, is truly profound; it took me a long time to appreciate it. Clearly, the class just uses the interface and there is less coupling, more flexibility, etc. But there is more:
The Ether
The power of DI is twofold:
(1) a given class is simplified and works with a minimal contract (the interface)
(2) there are tremendous tools available, outside the class, that allow us to really, truly make different implementations available, and swap 'em out easily. Essentially, these tools control the creation of the MyClass object, and allow us to dictate the given implementation for an interface (e.g. Map). Where are these implementations? Where do they come from? The answer is, from the view of the class: we don't know, they come out of the ether.
And that ether is powerful stuff: DI tools allow us to create mock implementations, single-threaded implementations, file-based implementations, DB implementations, carrier-pigeon implementations -- you name it. The configurations (or annotations) make it truly possible, unlike the hypothetical "free 2 weeks of tweaking paradise" mentioned earlier. You could potentially run several entirely different testing suites, simply by swapping out the ether.
To press the point even further, take the Map interface used above and now multiply this idea by any and all interfaces in your application. That ether is strong mojo.
Whether you use Guice or Spring: tap into this ether and re-discover the power of interfaces. If you're like me, you may discover they are cooler than we ever realized.
Final Score: Dependency Injection 1 Management 0
Post Script
Eric and others have made valid criticisms. See the article/comments, but here is a summary:
- I blew it on the phrase "any and all interfaces". The coolness of DI doesn't give one license to inject everything. A better version is: consider all of the interfaces in your project and then apply DI judiciously.
- My example was poor. I wanted to concentrate on the ether, and didn't have the energy to construct a "real-world" example so I went with Ye Olde Collections. I think it is an ok example to illustrate the ether, as long as one understands the criticisms against it. In fact, it might even be a better example because of the criticisms (though this was not by intent: mea culpa).
- I have used Spring's XML configuration, and have wondered about losing encapsulation as a danger of DI. I haven't yet tried Guice or any of Spring's newer stuff, so I don't know how that might mitigate the issue.
Couldn't agree less. Map is an implementation detail here. By making it part of the public interface, you've just created (not removed) a hard dependency. Now you can't change it to a List or a Set based implementation without completely screwing over any client code that's been built around it. Nice.
ReplyDeleteDave, I'm not sure I would go so far as to inject Map implementations either (@Inject @Hashed Map?), but with Guice, his constructor would not be part of his public API. Guice calls your constructors. Other classes just depend on your interface. They don't worry about how you get created, remember? As a client of your class, I don't want to know you need a Map, but I also don't want to know about any of your other deps.
ReplyDeleteThere have been valid criticisms of this post. I'll put a comment in a "ps" section in the post itself.
ReplyDelete"with Guice, his constructor would not be part of his public API"
ReplyDeleteI still don't buy it. Why write code to the Guice framework? Surely it's Guice's job to work with my classes, not the other way around? I thought that was kind of the point!
Eric's
ReplyDeletesite has a post that talks about Guice's use of constructors.
One way to think of it is this: as the class author, one gets to define the "gateway" (ctor) for clients and for DI tools.
If we don't define the initializer/ctor for the DI tools, then what choice do they have but to use bytecode manipulation, or to mandate that fields be protected/public, all of which would draw stronger criticism.
There's no free lunch! But the cost to "drink the Guice" :-} isn't very high
Why not just mandate that it has a constructor (or setter) that accepts the dependencies? Either they're implementation details and shouldn't be injected, or they're dependencies and should be available for any framework (including plain old code) to use.
ReplyDeleteIf it doesn't satisfy these requirements, it's not loosely coupled. If it does, it is - why does Guice demand special treatment?
If you provide a special constructor for some privileged framework to use, you're pretty much guaranteeing that other frameworks have less privileged access - why write your code to the one framework instead of following the loose coupling principle more generally?
In short, why do you feel the need to draw a distinction between "clients" and DI tools? Why aren't DI tools just clients?