[Update: Removed references to the company, because while I may rightfully be bitter, it is irrelevant to the content of this post, and I don't want the two issues to be mixed. Apologies to my readers.]
Jason Gorman, who I've had the pleasure of crossing the path of at one of my clients, is asking an interesting question about containers: are we hiding sweeping dependencies under the carpet?
Questioning the value of a container is a very recurring topic amongst developers that haven't been ripping their benefits, and I think it deserves a post in its own (that, and the fact that Jason's blog doesn't seem to let me post comments.)
First comes first, you have to think about what a container brings you, as it does bring you several things.
-
Dependency injection
This relieves you of having to chain objects that depend on each others, as the container is responsible to get a reference to each component your component needs.
This has the added benefit of enforcing the declaration of your dependencies in the component's constructor, making tracing dependencies much easier in a code review. It's also a good thing for discoverability of the code during test, as knowing what is required for your component to be put under test is very much in your face, on the first line, when you create the object. And that is a good thing.
-
Inversion of control
Your code no longer has to create the object to use it. You just don't have to think about it when you write your code. Of course, that doesn't mean you can ignore how the object is built, but this enforces a separation of concern: the consumer of the object doesn't have to know a whole lot to consume, and the creator of the object doesn't have to know much about the use of the object.
-
Lifetime management
Because you no longer use new, you can rely on the container to decide which object it gives you and where it exists. That means that asking for an IMyAppContext will retrieve the object from the HttpContext for you when it's needed, with no HttpModule or painful initialisation code anywhere.
And if you work on composite applications, you can create for the container a new way to store the objects, be it per component, per form, etc. You write that code once for the container, and never again will you have to care about the how.
Code reuse is a good thing.
-
Centralized component management
This one is less obvious but makes a lot of sense. When you want to know how an object is initialised and where, it's centralized in one place. One file gives you all the dependencies that exist in the system.
This also makes it possible to run code in your integration tests (or as a post deployment task) to ensure all dependencies are resolvable, and this, again, is a good thing.
-
Lower the cost of change
This one becomes obvious when you've been using containers for a few iterations. Taking on a new dependency is fantastically easy: add a parameter to the constructor and you are done. No consumer will ever have any code to change. Less code to change makes me happy.
There are many other things a container can do, but those points are shared across Unity, StructureMap and Windsor (I specifically ignore spring.net in my analysis, because I have next to none experience with it), and even by the COM+ catalog.
Jason probably knows each of those points very well. His doubts are about the centralized place in which those components are declared.
To a certain extent, I will agree on a small thing. Storing your dependencies in an xml document is not the most natural way to handle them, especially when you start playing with generics.
In the project I worked for in that unnamed media company, I built a small object builder as I was asked not to take more dependencies on external frameworks, hence I couldn't rely on a container. The dependencies were stored in the config file, and object instantiation is manual (no DI there, just IoC). A super-factory was a good first step to start decoupling components and get more visibility on what was going on in the code. But it's not as nice as having DI, and certainly not nice having types written in xml.
But you don't have to keep these things in config files. If you want to keep your dependencies as part of your project's code, go for it. Windsor has AddComponent, SturctureMap a fabulous Fluent api.
And if you don't want writing all that code, you can use Binsor for some boo love.
What I will strongly disagree with is part of Jason's analysis. A container doesn't hide dependencies from the compiler, because it can't. Component A implements interface A in assembly A. Component B depends on interface A and lives in assembly B. As far as I understand it, they all get compiled by a compiler. They are linked so dependencies are resolved when compiling the construction. That's why you declare your dependencies in the constructor!
But where Jason is right is highlighting that the configuration file is a dependency, and should be tested as such. This is exactly why you should have a stage environment that reproduce your live environment, and run your integration tests there. If your test don't detect a missing dependency declared in your config file, your tests are badly written or not extensive enough. You're just blaming the tool, blame the developer.
One thing that seems to be a common misconception is that you should know about your physical dependencies, aka knowing that component A depends on component B. This is the wrong way to look at the problem. Component B should depend on a contract it has for a service it's being provided. The glue between components is the container, and the guarantee that this works fine is your test. As for understanding your dependency graph, that's what code coverage and NDepends are there for. Anything else is only relevant to graphs people want to put in visio, which I would argue is irrelevant.
That said, it wouldn't take much work to trace those dependencies by asking the container. Maybe a day of work if you're into graphs. But if your dependencies bite you, you have a much bigger problem than tracing graphs, and that's usually a collapse in applying what is required for containers to work well: contracts, single responsibility principle, high cyclomatic complexity, poor code coverage, no integration tests, and self-containment of components to reduce complex interactions...