When to use a DI Container by Mark Seemann
This post explains why a DI Container is useful with Convention over Configuration while Poor Man's DI might be a better fit for a more explicit Composition Root.
Note (2018-07-18): Since I wrote this article, I've retired the term Poor Man's DI in favour of Pure DI.
It seems to me that lately there's been a backlash against DI Containers among alpha geeks. Many of the software leaders that I myself learn from seem to dismiss the entire concept of a DI Container, claiming that it's too complex, too 'magical', that it isn't a good architectural pattern, or that the derived value doesn't warrant the 'cost' (most, if not all, DI Containers are open source, so they are free in a monetary sense, but there's always a cost in learning curve etc.).
This must have caused Krzysztof Koźmic to write a nice article about what sort of problem a DI Container solves. I agree with the article, but want to provide a different perspective here.
In short, it makes sense to me to illustrate the tradeoffs of Poor Man's DI versus DI Containers in a diagram like this:
The point of the diagram is that Poor Man's DI can be valuable because it's simple, while a DI Container can be either valuable or pointless depending on how it's used. However, when used in a sufficiently sophisticated way I consider a DI Container to offer the best value/cost ratio. When people criticize DI Containers as being pointless I suspect that what really happened was that they gave up before they were out of the Trough of Disillusionment. Had they continued to learn, they might have arrived at a new Plateau of Productivity.
DI style | Advantages | Disadvantages |
---|---|---|
Poor Man's DI |
|
|
Explicit Register |
|
|
Convention over Configuration |
|
|
There are other, less important advantages and disadvantages of each approach, but here I'm focusing on three main axes that I consider important:
- How easy is it to understand and learn?
- How soon will you get feedback if something is not right?
- How easy is it to maintain?
The major advantage of Poor Man's DI is that it's easy to learn. You don't have to learn the API of any DI Container (Unity, Autofac, Ninject, StructureMap, Castle Windsor, etc.) and while individual classes still use DI, once you find the Composition Root it'll be evident what's going on and how object graphs are constructed. No 'magic' is involved.
The second big advantage of Poor Man's DI is often overlooked: it's strongly typed. This is an advantage because it provides the fastest feedback about correctness that you can get. However, strong typing cuts both ways because it also means that every time you refactor a constructor, you will break the Composition Root. If you are sharing a library (Domain Model, Utility, Data Access component, etc.) between more than one application (unit of deployment), you may have more than one Composition Root to maintain. How much of a burden this is depends on how often you refactor constructors, but I've seen projects where this happens several times each day (keep in mind that constructor are implementation details).
If you use a DI Container, but explicitly Register each and every component using the container's API, you lose the rapid feedback from strong typing. On the other hand, the maintenance burden is also likely to drop because of Auto-wiring. Still, you'll need to register each new class or interface when you introduce them, and you (and your team) still has to learn the specific API of that container. In my opinion, you lose more advantages than you gain.
Ultimately, if you can wield a DI Container in a sufficiently sophisticated way, you can use it to define a set of conventions. These conventions define a rule set that your code should adhere to, and as long as you stick to those rules, things just work. The container drops to the background, and you rarely need to touch it. Yes, this is hard to learn, and is still weakly typed, but if done right, it enables you to focus on code that adds value instead of infrastructure. An additional advantage is that it creates a positive feedback mechanism forcing a team to produce code that is consistent with the conventions.
Example: Poor Man's DI #
The following example is part of my Booking sample application. It shows the state of the Ploeh.Samples.Booking.Daemon.Program class as it looks in the git tag total-complexity (git commit ID 64b7b670fff9560d8947dd133ae54779d867a451).
var queueDirectory = new DirectoryInfo(@"..\..\..\BookingWebUI\Queue").CreateIfAbsent(); var singleSourceOfTruthDirectory = new DirectoryInfo(@"..\..\..\BookingWebUI\SSoT").CreateIfAbsent(); var viewStoreDirectory = new DirectoryInfo(@"..\..\..\BookingWebUI\ViewStore").CreateIfAbsent(); var extension = "txt"; var fileDateStore = new FileDateStore( singleSourceOfTruthDirectory, extension); var quickenings = new IQuickening[] { new RequestReservationCommand.Quickening(), new ReservationAcceptedEvent.Quickening(), new ReservationRejectedEvent.Quickening(), new CapacityReservedEvent.Quickening(), new SoldOutEvent.Quickening() }; var disposable = new CompositeDisposable(); var messageDispatcher = new Subject<object>(); disposable.Add( messageDispatcher.Subscribe( new Dispatcher<RequestReservationCommand>( new CapacityGate( new JsonCapacityRepository( fileDateStore, fileDateStore, quickenings), new JsonChannel<ReservationAcceptedEvent>( new FileQueueWriter<ReservationAcceptedEvent>( queueDirectory, extension)), new JsonChannel<ReservationRejectedEvent>( new FileQueueWriter<ReservationRejectedEvent>( queueDirectory, extension)), new JsonChannel<SoldOutEvent>( new FileQueueWriter<SoldOutEvent>( queueDirectory, extension)))))); disposable.Add( messageDispatcher.Subscribe( new Dispatcher<SoldOutEvent>( new MonthViewUpdater( new FileMonthViewStore( viewStoreDirectory, extension))))); var q = new QueueConsumer( new FileQueue( queueDirectory, extension), new JsonStreamObserver( quickenings, messageDispatcher)); RunUntilStopped(q);
Yes, that's a lot of code. I deliberately chose a non-trivial example to highlight just how much stuff there might be. You don't have to read and understand all of this code to appreciate that it might require a bit of maintenance. It's a big object graph, with some shared subgraphs, and since it uses the new keyword to create all the objects, every time you change a constructor signature, you'll need to update this code, because it's not going to compile until you do.
Still, there's no 'magical' tool (read: DI Container) involved, so it's pretty easy to understand what's going on here. As Dan North put it once I saw him endorse this technique: 'new' is the new 'new' :) Once you see how Explicit Register looks, you may appreciate why.
Example: Explicit Register #
The following example performs exactly the same work as the previous example, but now in a state (git tag: controllers-by-convention; commit ID: 13fc576b729cdddd5ec53f1db907ec0a7d00836b) where it's being wired by Castle Windsor. The name of this class is DaemonWindsorInstaller, and all components are explictly registered. Hang on to something.
container.Register(Component .For<DirectoryInfo>() .UsingFactoryMethod(() => new DirectoryInfo(@"..\..\..\BookingWebUI\Queue").CreateIfAbsent()) .Named("queueDirectory")); container.Register(Component .For<DirectoryInfo>() .UsingFactoryMethod(() => new DirectoryInfo(@"..\..\..\BookingWebUI\SSoT").CreateIfAbsent()) .Named("ssotDirectory")); container.Register(Component .For<DirectoryInfo>() .UsingFactoryMethod(() => new DirectoryInfo(@"..\..\..\BookingWebUI\ViewStore").CreateIfAbsent()) .Named("viewStoreDirectory")); container.Register(Component .For<IQueue>() .ImplementedBy<FileQueue>() .DependsOn( Dependency.OnComponent("directory", "queueDirectory"), Dependency.OnValue("extension", "txt"))); container.Register(Component .For<IStoreWriter<DateTime>, IStoreReader<DateTime>>() .ImplementedBy<FileDateStore>() .DependsOn( Dependency.OnComponent("directory", "ssotDirectory"), Dependency.OnValue("extension", "txt"))); container.Register(Component .For<IStoreWriter<ReservationAcceptedEvent>>() .ImplementedBy<FileQueueWriter<ReservationAcceptedEvent>>() .DependsOn( Dependency.OnComponent("directory", "queueDirectory"), Dependency.OnValue("extension", "txt"))); container.Register(Component .For<IStoreWriter<ReservationRejectedEvent>>() .ImplementedBy<FileQueueWriter<ReservationRejectedEvent>>() .DependsOn( Dependency.OnComponent("directory", "queueDirectory"), Dependency.OnValue("extension", "txt"))); container.Register(Component .For<IStoreWriter<SoldOutEvent>>() .ImplementedBy<FileQueueWriter<SoldOutEvent>>() .DependsOn( Dependency.OnComponent("directory", "queueDirectory"), Dependency.OnValue("extension", "txt"))); container.Register(Component .For<IChannel<ReservationAcceptedEvent>>() .ImplementedBy<JsonChannel<ReservationAcceptedEvent>>()); container.Register(Component .For<IChannel<ReservationRejectedEvent>>() .ImplementedBy<JsonChannel<ReservationRejectedEvent>>()); container.Register(Component .For<IChannel<SoldOutEvent>>() .ImplementedBy<JsonChannel<SoldOutEvent>>()); container.Register(Component .For<ICapacityRepository>() .ImplementedBy<JsonCapacityRepository>()); container.Register(Component .For<IConsumer<RequestReservationCommand>>() .ImplementedBy<CapacityGate>()); container.Register(Component .For<IConsumer<SoldOutEvent>>() .ImplementedBy<MonthViewUpdater>()); container.Register(Component .For<Dispatcher<RequestReservationCommand>>()); container.Register(Component .For<Dispatcher<SoldOutEvent>>()); container.Register(Component .For<IObserver<Stream>>() .ImplementedBy<JsonStreamObserver>()); container.Register(Component .For<IObserver<DateTime>>() .ImplementedBy<FileMonthViewStore>() .DependsOn( Dependency.OnComponent("directory", "viewStoreDirectory"), Dependency.OnValue("extension", "txt"))); container.Register(Component .For<IObserver<object>>() .UsingFactoryMethod(k => { var messageDispatcher = new Subject<object>(); messageDispatcher.Subscribe(k.Resolve<Dispatcher<RequestReservationCommand>>()); messageDispatcher.Subscribe(k.Resolve<Dispatcher<SoldOutEvent>>()); return messageDispatcher; })); container.Register(Component .For<IQuickening>() .ImplementedBy<RequestReservationCommand.Quickening>()); container.Register(Component .For<IQuickening>() .ImplementedBy<ReservationAcceptedEvent.Quickening>()); container.Register(Component .For<IQuickening>() .ImplementedBy<ReservationRejectedEvent.Quickening>()); container.Register(Component .For<IQuickening>() .ImplementedBy<CapacityReservedEvent.Quickening>()); container.Register(Component .For<IQuickening>() .ImplementedBy<SoldOutEvent.Quickening>()); container.Register(Component .For<QueueConsumer>()); container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel));
This is actually more verbose than before - almost double the size of the Poor Man's DI example. To add spite to injury, this is no longer strongly typed in the sense that you'll no longer get any compiler errors if you change something, but a change to your classes can easily lead to a runtime exception, since something may not be correctly configured.
This example uses the Registration API of Castle Windsor, but imagine the horror if you were to use XML configuration instead.
Other DI Containers have similar Registration APIs (apart from those that only support XML), so this problem isn't isolated to Castle Windsor only. It's inherent in the Explicit Register style.
I can't claim to be an expert in Java, but all I've ever heard and seen of DI Containers in Java (Spring, Guice, Pico), they don't seem to have Registration APIs much more sophisticated than that. In fact, many of them still seem to be heavily focused on XML Registration. If that's the case, it's no wonder many software thought leaders (like Dan North with his 'new' is the new 'new' line) dismiss DI Containers as being essentially pointless. If there weren't a more sophisticated option, I would tend to agree.
Example: Convention over Configuration #
This is still the same example as before, but now in a state (git tag: services-by-convention-in-daemon; git commit ID: 0a7e6f246cacdbefc8f6933fc84b024774d02038) where almost the entire configuration is done by convention.
container.AddFacility<ConsumerConvention>(); container.Register(Component .For<IObserver<object>>() .ImplementedBy<CompositeObserver<object>>()); container.Register(Classes .FromAssemblyInDirectory(new AssemblyFilter(".").FilterByName(an => an.Name.StartsWith("Ploeh.Samples.Booking"))) .Where(t => !(t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dispatcher<>))) .WithServiceAllInterfaces()); container.Kernel.Resolver.AddSubResolver(new ExtensionConvention()); container.Kernel.Resolver.AddSubResolver(new DirectoryConvention(container.Kernel)); container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel)); #region Manual configuration that requires maintenance container.Register(Component .For<DirectoryInfo>() .UsingFactoryMethod(() => new DirectoryInfo(@"..\..\..\BookingWebUI\Queue").CreateIfAbsent()) .Named("queueDirectory")); container.Register(Component .For<DirectoryInfo>() .UsingFactoryMethod(() => new DirectoryInfo(@"..\..\..\BookingWebUI\SSoT").CreateIfAbsent()) .Named("ssotDirectory")); container.Register(Component .For<DirectoryInfo>() .UsingFactoryMethod(() => new DirectoryInfo(@"..\..\..\BookingWebUI\ViewStore").CreateIfAbsent()) .Named("viewStoreDirectory")); #endregion
It's pretty clear that this is a lot less verbose - and then I even left three explicit Register statements as a deliberate decision. Just because you decide to use Convention over Configuration doesn't mean that you have to stick to this principle 100 %.
Compared to the previous example, this requires a lot less maintenance. While you are working with this code base, most of the time you can concentrate on adding new functionality to the software, and the conventions are just going to pick up your changes and new classes and interfaces. Personally, this is where I find the best tradeoff between the value provided by a DI Container versus the cost of figuring out how to implement the conventions. You should also keep in mind that once you've learned to use a particular DI Container like this, the cost goes down.
Summary #
Using a DI Container to compose object graphs by convention presents an unparalled opportunity to push infrastructure code to the background. However, if you're not prepared to go all the way, Poor Man's DI may actually be a better option. Don't use a DI Container just to use one. Understand the value and cost associated with it, and always keep in mind that Poor Man's DI is a valid alternative.
Comments
Testing usually seems to be the pressure release for not using an IoC container in a static language. Even if it's just a matter of not writing the tests for the overridden constructor, testing is usually thrown out. And in a world without tests, Poor Man's DI (or not DI at all) is often the "simpler" solution. Less lines of code, "it just works," etc etc. There are lots of options when you only look at the implementation without concern about how one is to provide automated verification against regressions.
If using TDD or even just "testing," an IoC container is always the simpler solution. Unless, of course -- if you just switch to a language or framework that lets you do both. *cough* ruby *cough* python *cough* dynamic languages *cough*
Just to add another perspective:
I created some extension methods in one of my core libraries that registers everything that ends with Service, Factory, Provider etc. Additionally, I created some extension methods for special areas like NHibernate or AutoMapping.
With these extension methods and a project that adheres to these conventions, my composition roots are very short and need virtually no maintenance.
I have successfully used this approach in several mid to big sized projects. It just works, I wouldn't want to work without a DI container anymore as it would cost so much more time.
1.) A constructor with no arguments. Dependencies are initialized in the constructor. This constructor can be used to instantiate the object like "myThing = new MyThing();" This is not using DI at all.
2.) A constructor with arguments for each dependency. Dependencies are passed in. This constructor is used for testing, since it actually uses DI.
This "Poor Man's DI" is a concept because it's a cheap way to get DI into a class that may not have originally been written to support DI. In a way, it seems to give devs the best of both worlds. Users can still instantiate the class simply, but users can also test it. It sounds fine, but it has some issues because the class is still bound to its dependencies and because the implementation uses different code than the tests.
Looking deeper at your example, I see that's not what your "Poor Man's DI" example is. Your way is fully testable, but I don't think its deserving of the extra "Poor Man's DI" moniker because it's just hand-rolled class instantiation. Or to put it another way: If your code is an example of "Poor Man's DI," then wouldn't any DI that wasn't handled through an IoC container? You are just creating objects with code -- nothing special. (or wrong, either)
If that's what "Poor Man's DI" means, there should probably be a new phrase to identify the practice that I've seen the phrase tied to -- as it's a "special" and unique practice. (Take that however you will. :) )
Now that I think about it,the concept of "Poor Man's DI" and "Bastard Injection" seem to refer to different things. Given your definition, Poor Man's DI basically seems to mean that I don't use an IoC container. It's a concept defining the method in which the object is created. But Bastard Injection refers to what I think would be the more common use of "Poor Man's DI," the practice of creating two constructors. It's a concept defining the method in which the class itself is written. I guess, then, it's possible for me to use Bastard Injection with Poor Man's DI, so long as I don't call the default constructor?
As one more side note: I really don't like the name "Bastard Injection" due to the coarse language. I know it's an anti-pattern, but "bastard" is a word I'd never ever accept from myself or other developers in a professional setting, especially with a client. I just asked my wife, an public elementary school teacher and librarian, and she said that word would not be accepted in her class or at any school she's been at. I don't think it's helpful to give PG13-level words to programming concepts. :)
How do considerations of lifetime management factor in? I may want Singleton here, Transient there, etc. That would seem to favor Explicit Register.
There's also the option of integration-testing the Composition Root to provide some type-checking.
When it comes to lifetime management, there are answers on more than one level.
On the pragmatic level, I've often found that in reality, most of my graphs tend to need to be Transient (or Per Graph) because some commonly used leaf node must be Transient (or Per Graph) for whatever reason. Once that happens, if most (say: more than 75%) of all objects are already Transient, does it really matter if a few more are also Transient? Yes, it could have been more efficient, but if you profile your app, you're most likely to discover that your bottleneck is somewhere else entirely.
On a more explicit level, it would be possible to define a convention that picks up a hint about the desired lifetime from the type itself. You could for example call a service "ThreadSafeFoo" to hint that Singleton would be appropriate - or perhaps you could adorn it with a [ThreadSafe] attribute...
Testing the container itself I don't find particularly useful, but a set of smoke tests of the entire app can be helpful.
I may be wrong, but reading your post I understand that the goal of a DI Container is to compose object graphs. This is undoubtedly true. Yet I think that this is just one of DI Containers' goals, and possibly not even the main one.
I'm sure you already know the amazing post An Autofac Lifetime Primer by Nicholas Blumhardt. It is about AutoFac, but it covers principles that are common to all the CI Containers.
Reading Nicholas post what I get is that a CI Container is a tool whose main goal is to manage resources lifetimes. Nicholas defines a resource as "anything with acquisition and release phases in its lifecycle". IoC Containers "provide a good solution to the resource management problem" and "to do this, they need to take ownership of the disposable components that they create". In other words, not only do DI Containers compose object graphs, but they also take care of the lifecycle of objects they created. Nicholas post is very detailed in explaining how and why a DI Container must track resources and guarantee that their disposal is properly managed.
This is an excerpt I find particurarly significant:
"[...] you need to find a strategy to ensure resources are disposed when they’re no longer required. The most widely-attempted one is based around the idea that whatever object acquires the resource should also release it. I pejoratively call it “ad-hoc” because it doesn’t work consistently. Eventually you’ll come up against one (and likely more) of the following issues:
Sharing: When multiple independent components share a resource, it is very hard to figure out when none of them requires it any more. Either a third party will have to know about all of the potential users of the resource, or the users will have to collaborate. Either way, things get hard fast.
Cascading Changes: Let’s say we have three components – A uses B which uses C. If no resources are involved, then no thought needs to be given to how resource ownership or release works. But, if the application changes so that C must now own a disposable resource, then both A and B will probably have to change to signal appropriately (via disposal) when that resource is no longer needed. The more components involved, the nastier this one is to unravel."
CI Containers solve these problems.
Poor Man (or Pure) CI solves the compose phase only. But the CI should also take care of resource disposal, or it would not provide any Unit of Work and possibly lead to memory leaks or NullPointerExceptions at runtime. What a basic Por Man implementation provides is just an Instance Per Dependency Scope (every request gets a new instance). With few modificatios, it could provide a Single Instance Scope (that is, a Singleton). But you might agree that managing nested scopes, shared dependencies, instances per web request and a proper disposal management with a Poor Man CI is all but a simple task.
So, I'm not sure the distinction between Poor Man and DI Containers is only a matter of Convention over Configuration. I got to the conclusion that the main goal of a DI Container is lifecycle management, much more than object graph composition.
What do you think?
Arialdo, thank you for writing.
Like you, I used to think that lifetime management was an strong motivation to use a DI Container; there's an entire chapter about lifetime management in my book.
There may be cases where that's true, but these days I prefer the the explicit lifetime matching I get from Pure DI.
While you can make lifetime management quite complicated, I prefer to keep it simple, so in practice, I only use the Singleton and Transient lifetime styles. Additionally, I prefer to design my components so that they aren't disposable. If I must use a disposable third-party object, my next priority would be to use a Decoraptor, and add decommissioning support if necessary. Only if none of that is possible will I begin to look at disposal from the Composition Root.
Usually, when you only use Singleton and Transient, manual disposal from the Composition Root is easy. There's no practical reason to dispose of the Singletons, so you only need to dispose of the Transient objects. How you do that varies from framework to framework, but in ASP.NET Web API, for example, it's easy.