BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Succeeding with Dependency Injection

Succeeding with Dependency Injection

Bookmarks

While I was writing Dependency Injection in .NET, a common reaction I got was "how can you write a whole book about Dependency Injection?" That sort of incredulous reaction is natural when you consider how easy it is to grasp the central pattern (Constructor Injection).

However, while the principal pattern is easy to understand, it can be difficult to succeed with Dependency Injection (DI), since the mechanics are only one part of a larger context. DI is an application of the principle of Inversion of Control (IoC) and to succeed with IoC you'll also need to invert your thinking. This article provides a sketch of the mental model you need to adopt to succeed with DI.

Loose coupling: Dependency Injection vs. Service Locator

If you don't understand the purpose of DI it's easy to implement it incorrectly. Here's a recent example I saw:

private readonly ILog log;

public MyConsumer(ILog log)
{
    this.log = log ?? LogManager.GetLogger("My");
}

In terms of encapsulation, the main problem with such an approach is that it seems like the MyConsumer class can't really make up its mind whether or not it controls the creation of its log dependency. While this is a simplified example, this could become a problem if the ILog instance returned by LogManager wraps an unmanaged resource which should be disposed when it's no longer needed.

Implementations like this tend to pop up because the developer focuses entirely on making MyConsumer testable with unit tests. The rationale behind this being the developer wants to be able to replace ILog only for purposes of unit testing. In all other purposes, the instance returned by LogManager should be used.

This is essentially the Bastard Injection anti-pattern in action. One of the potential problems is that it becomes very easy to violate the Liskov Substitution Principle because one particular implementation receives special treatment.

It's important to realize that the purpose of DI is much broader than just enabling unit testing. The purpose is to enable loose coupling in order to increase maintainability of the overall solution. (If you wonder why loose coupling increases maintainability, chapter 1 of my book discusses this subject and is available for free as a preview.)

Loose coupling can be summarized as the concept of programming to an interface instead of a concrete implementation. However, since interfaces don't have constructors, it very quickly raises the question of how to create instances of those interfaces.

There are two radically different approaches to get instances of the interfaces against which you'd like to program:

With the approach used above, it would be very tempting to move towards the Service Locator anti-pattern, like this:

public MyConsumer()
{
    this.log = Locator.Resolve<ILog>();
}

In addition to other problems with Service Locator, the problem with this approach is that the parameter used with LogManager.GetLogger("My") method call is now lost. Assuming other consumers may need other loggers instantiated with different parameters, this version of Service Locator won't work.

This often leads to one or more overloads to the Resolve method on the Locator in order to supply information about the context to the Service Locator. A violation of the Liskov Substitution Principle is not far behind.

DI offers a better approach:

private readonly ILog log;

public MyConsumer(ILog log)
{
    if (log == null)
        throw new ArgumentNullException("log");
    this.log = log;
}

This is Inversion of Control in its pure form. Any implementation of ILog is accepted while a Guard Clause guarantees that the instance can't be null. Contrary to using a Service Locator, the context isn't lost when the object graph is composed.

var consumer = new MyConsumer(
    LogManager.GetLogger("My"));

At the time when the MyConsumer instance is created, the Composer knows the (class) identity of this particular consumer of the ILog interface, and can thus supply the correct implementation given the context.

DI and Service Locator are two mutually exclusive approaches to enable loose coupling. On the technical level, both can be made to work, but DI comes with none of the disadvantages Service Locator drags along.

The only disadvantage of DI is that it's not as immediately understandable as Service Locator. To succeed with DI you need to overcome a few obstacles.

The mountain road to Dependency Injection

One of the challenges with learning DI is that the hardest problem is the first one you encounter: How do you get an instance of an interface? The good news is once you've understood that Constructor Injection simply implies a request for the instance through the constructor, you're over the worst obstacle.

The next challenge will be easier to solve, and the next one easier again. I like to think of these challenges as mountains you have to climb. The first one is tall and steep, but already the next one is much easier, and from then on it quickly becomes a smooth ride:

The first speed bump on your path to success with DI is to understand that with Constructor Injection, you delegate the responsibility of composing your consumer with its dependency to a third party. It's important to realize that this third party is the Composition Root, which is a single point in an application where the entire object graph is composed at once. Since the Composition Root composes the entire object graph, it has access to the entire context, which enables it to make intelligent decisions about which dependencies go where.

This scares some developers because they are afraid this will be bad for performance, but that's not the case.

The second speed bump for most people seems to occur when a run-time value is required to resolve a dependency. This is often the case when a dependency can't be resolved until the user has made a specific choice in the user interface. In this case an Abstract Factory is often a solution.

In my experience, the first two obstacles tend to be the most difficult to overcome. Other challenges will appear, but these tend to be more individual. In chapter 6 of my book I've collected the most common types of problems people run into, and how they can be resolved.

In any case, once you have the correct mental model about DI, any challenge will be easy to solve.

About the Author

Mark Seemann is the creator of AutoFixture and the author of Dependency Injection in .NET. He is a professional software developer and architect living in Copenhagen, Denmark, and currently a Software Architect for Commentor, a Danish consulting company. He enjoys reading, drawing, playing the guitar, good wine, and gourmet food.

 

 

Rate this Article

Adoption
Style

BT