A Dependency Injection anti-pattern.

Once in a while, someone comes up with the idea that it would be great to introduce a common abstraction over various DI Containers in .NET. My guess is that part of the reason for this is that there are so many DI Containers to choose from on .NET:

... and these are just the major ones; there are many more! Hiding all these different libraries behind a common interface sounds like a smashing idea, but isn't.

General form

At its core, a Conforming Container introduces a central interface, often called IContainer, IServiceLocator, IServiceProvider, ITypeActivator, IServiceFactory, or something in that vein. The interface defines one or more methods called Resolve, Create, GetInstance, or similar:

public interface IContainer
{
    object Resolve(Type type);
 
    object Resolve(Type type, params object[] arguments);
 
    T Resolve<T>();
 
    T Resolve<T>(params object[] arguments);
 
    IEnumerable<T> ResolveAll<T>();
 
    // etc.
}

Sometimes, the interface defines only a single of those methods; sometimes, it defines even more variations of methods to create objects based on a Type.

Some Conforming Containers stop at this point, so that the interface only exposes Queries, which means that they only cover the Resolve phase of the Register Resolve Release pattern. Other efforts attempt to address Register phase too:

public interface IContainer
{
    void AddService(Type serviceType, Type implementationType);
 
    void AddService<TService, TImplementation>();
 
    // etc.
}

The intent is to enable configuration of the container using some sort of metadata. Sometimes, the methods have more advanced configuration parameters that also enable you to specify the lifestyle of the service, etc.

Finally, a part of a typical Conforming Container ecosystem is various published Adapters to concrete DI Containers. A hypothetical Confainer project may publish the following Adapter packages:

  • Confainer.Autofac
  • Confainer.Windsor
  • Confainer.Ninject
  • Confainer.Unity
Notice that in this example, not all major .NET DI Containers are listed. This is a typical situation. Obviously, since the entire effort is to define an interface, contributors are often invited to provide Adapters for missing DI Containers.

Symptoms and consequences

A Conforming Container is an anti-pattern, because it's

a commonly occurring solution to a problem that generates decidedly negative consequences,
such as:
  • Calls to the Conforming Container are likely to be sprinkled liberally over an entire code base.
  • It pushes novice users towards the Service Locator anti-pattern. Most people encountering Dependency Injection for the first time mistake it for the Service Locator anti-pattern, despite the entirely opposite natures of these two approaches to loose coupling.
  • It attempts to relieve symptoms of bad design, instead of addressing the underlying problem. Too many 'loosely coupled' designs attempt to rely on the Service Locator anti-pattern, which, by default, introduces a dependency to a concrete Service Locator throughout a code base. However, exclusively using the Constructor Injection and Composition Root design patterns eliminate the problem altogether, resulting in a simpler design with fewer moving parts.
  • It pulls in the direction of the lowest common denominator.
  • It stifles innovation, because new, creative, but radical ideas may not fit into the narrow view of the world a Conforming Container defines.
  • It makes it more difficult to avoid using a DI Container. A DI Container can be useful in certain scenarios, but often, hand-coded composition is better than using a DI Container. However, if a library or framework depends on a Conforming Container, it may be difficult to harvest the benefits of hand-coded composition.
  • It may introduce versioning hell. Imagine that you need to use a library that depends on Confainer 1.3.7 in an application that also uses a framework that depends on Confainer 2.1.7. Since a Conforming Container is intended as an infrastructure component, this is likely to happen, and to cause much grief.
  • A Conforming Container is often a product of Speculative Generality, instead of a product of need. As such, the API is likely to be poorly suited to address real-world scenarios, be difficult to extent, and may exhibit churn in the form of frequent breaking changes.
  • If Adapters are supplied by contributors (often the DI Container maintainers themselves), the Adapters may have varying quality levels, and may not support the latest version of the Conforming Container.

A code base using a Conforming Container may have code like this all over the place:

var foo = container.Resolve<IFoo>();
// ... use foo for something...
 
var bar = container.Resolve<IBar>();
// ... use bar for something else...
 
var baz = container.Resolve<IBaz>();
// ... use baz for something else again...

This breaks encapsulation, because it's impossible to identify a class' collaborators without reading its entire code base.

Additionally, concrete DI Containers have distinct feature sets. Although likely to be out of date by now, this feature comparison chart from my book illustrate this point:

Castle WindsorStructureMapSpring.NETAutofacUnityMEF
Code as Configuration x x x x
Auto-registration x x x
XML configuration x x x x x
Modular configuration x x x x x x
Custom lifetimes x x (x) x
Decommissioning x x (x) x
Interception x x x

This is only a simple chart that plots the most common features of DI Containers. Each DI Container has dozens of features - many of them unique to that particular DI Container. A Conforming Container can either support an intersection or union of all those features.

Intersection and union of containers

A Conforming Container that targets only the intersection of all features will be able to support only a small fraction of all available features, diminishing the value of the Conforming Container to the point where it becomes gratuitous.

A Conforming Container that targets the union of all features is guaranteed to consist mostly of a multitude of NotImlementedExceptions, or, put in another way, massively breaking the Liskov Substitution Principle.

Typical causes

The typical causes of the Conforming Container anti-pattern are:

  • Lack of understanding of Dependency Injection. Dependency Injection is a set of patterns driven by the Dependency Inversion Principle. A DI Container is an optional library, not a required part.
  • A fear of letting an entire code base depend on a concrete DI Container, if that container turns out to be a poor choice. Few programmers have thouroughly surveyed all available DI Containers before picking one for a project, so architects desire to have the ability to replace e.g. StructureMap with Ninject.
  • Library designers mistakenly thinking that Dependency Injection support involves defining a Conforming Container.
  • Framework designers mistakenly thinking that Dependency Injection support involves defining a Conforming Container.
The root cause is always a lack of awareness of a simpler solution.

Known exceptions

There are no cases known to me where a Conforming Container is a good solution to the problem at hand. There's always a better and simpler solution.

Refactored solution

Instead of relying on the Service Locator anti-pattern, all collaborating classes should rely on the Constructor Injection pattern:

public class CorrectClient
{
    private readonly IFoo foo;
    private readonly IBar bar;
    private readonly IBaz baz;
 
    public CorrectClient(IFoo foo, IBar bar, IBaz baz)
    {
        this.foo = foo;
        this.bar = bar;
        this.baz = baz;
    }
 
    public void DoSomething()
    {
        // ... use this.foo for something...
 
        // ... use this.bar for something else...
 
        // ... use this.baz for something else again...
    }
}

This leaves all options open for any code consuming the CorrectClient class. The only exception to relying on Constructor Injection is when you need to compose all these collaborating classes. The Composition Root has the single responsibility of composing all the objects into a working object graph:

public class CompositionRoot
{
    public CorrectClient ComposeClient()
    {
        return new CorrectClient(
            new RealFoo(),
            new RealBar(),
            new RealBaz());
    }
}

In this example, the final graph is rather shallow, but it can be as complex and deep as necessary. This Composition Root uses hand-coded composition, but if you want to use a DI Container, the Composition Root is where you put it:

public class WindsorCompositionRoot
{
    private readonly WindsorContainer container;
 
    public WindsorCompositionRoot()
    {
        this.container = new WindsorContainer();
        // Configure the container here,
        // or better yet: use a WindsorInstaller
    }
 
    public CorrectClient ComposeClient()
    {
        return this.container.Resolve<CorrectClient>();
    }
}

This class (and perhaps a few auxiliary classes, such as a Windsor Installer) is the only class that uses a concrete DI Container. This is the Hollywood Principle in action. There's no reason to hide the DI Container behind an interface, because it has no clients. The DI Containers knows about the application; the application knows nothing about the DI Container.

In all but the most trivial of applications, the Composition Root is only an extremely small part of the entire application.

A Composition Root is only a small part of an application

(The above picture is meant to illustrate an arbitrary application architecture; it could be layered, onion, hexagonal, or something else - it doesn't really matter.) If you want to replace one DI Container with another DI Container, you only replace the Composition Root; the rest of the application will never notice the difference.

Notice that only applications should have Composition Roots. Libraries and frameworks should not.

  • Library classes should be defined with Constructor Injection throughout. If the library object model is very complex, a few Facades can be supplied to make it easier for library users to get started. See my article on DI-friendly libraries for more details.
  • Frameworks should have appropriate hooks built in. These hooks should not be designed as Service Locators, but rather as Abstract Factories. See my article on DI-friendly frameworks for more details.
These solutions are better than a Conforming Container because they are simpler, have fewer moving parts, are easier to understand, and easier to reason about.

Variations

Sometimes the Conforming Container only defines a Service Locator-like API, and sometimes it also defines a configuration API. That configuration API may include various axes of configurability, most notably lifetime management and decommisioning.

Decommissioning is often designed around the concept of a disposable 'context' scope, but as I explain in my book, that's not an extensible pattern.

Known examples

There are various known examples of Conforming Containers for .NET:

Additionally, it looks like the new Dependency Injection support for ASP.NET is taking this route as well, although hopefully, it's not too late to change that.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Monday, 19 May 2014 07:54:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!