Dependency Injection and Inversion of Control Containers

In future posts, I plan on diving into Ninject 2. As I was writing that post, I found myself covering a lot of background material on dependency injection (DI) and inversion of control (IOC) containers. This is necessary material if you are not familiar with DI and IOC. If you already understand these concepts then check back soon for material on how you can become a ninja.

What is Dependency Injection?

Dependency injection (DI) is a specific form of inversion of control (IOC) where the concern being inverted is the process of obtaining a dependency. Without DI the consumer of a particular service would be responsible for handling the creation of the service object(s). Let’s look at a contrived example.

public class FileLogger
{
    private const string _fileName = @"c:templog.txt";

    public FileLogger()
    {
    }

    public void WriteLine(string line)
    {
        File.AppendAllText(_fileName, line);
    }
}

An instance of ‘FileLogger’ has two dependencies: the file name (‘_fileName’) and the file system (static ‘System.IO.File’ class). Besides being a naive implementation of a logger class, what is wrong with the class controlling the creation of these two dependencies?

Because the file name dependency is created inside the class, this logger implementation will always write log data to a specific file. Without some refactoring there is zero hope of reusing this class to write log data elsewhere. For something that is not domain specific (like a logger), code should be written in such a way that it can be reused.

The file system dependency is a bit tricky. In the above implementation, objects of ‘FileLogger’ type will always use the default file system (‘System.IO.File’). Why would you ever want to use another file system? In the normal operation of the ‘FileLogger’ class you probably do want to use the default file system. The issue is that automated testing of the ‘FileLogger’ class will involve writing / reading files to the real file system. To keep test execution speed high, writing to the real file system (or database) is generally frowned upon.

Another code metric that is used to describe the code quality is coupling. The current implementation of the file logger is highly coupled to the file system implementation. A software engineering best practice is to select designs that reduce the amount of coupling in thee application. So how would we reduce coupling in the above case?

public class FileLogger
{
    private readonly IFileSystem _fileSystem;
    private readonly string _fileName;

    public FileLogger(IFileSystem fileSystem, string fileName)
    {
        // Input validation code goes here...

        _fileSystem = fileSystem;
        _fileName = fileName;

        // Code to ensure the directory for the file exists...
    }

    public void WriteLine(string line)
    {
        _fileSystem.AppendAllText(_fileName, line);
    }
}

In the new version of the ‘FileLogger’ class we have used dependency injection (DI) to inject the two dependencies that we have into the constructor (construction injection). This allows the application that is using the ‘FileLogger’ object to set the file name and file system. Now we can easily log to multiple log files and our test bed can use an in-memory fake file system.

Another consideration of using the ‘FileLogger’ class is object life times. Typically, loggers are used throughout the application. A logger object is generally created once and that instance is fetched by all subsequent requests. This typically leads to some form of singleton implementation. As we have seen, singleton implementations that rely upon static instances can have implications on testing.  Another option is to have the application framework create an instance of the logger and then provide a globally available mechanism (cache for instance) to fetch it.

Life time (or life-cycle) issues have well known solutions. However, these solutions typically add a layer of complexity. Wouldn’t it be nice to abstract away the creation / life-cycle concerns? That is the purpose of an IOC container.

What is IOC Container?

An IOC container is an abstraction that resolves and creates dependencies. Developers can write highly decoupled code and not worry (as much) about how the pieces get put together. The IOC container is responsible for gluing together these highly decoupled objects to create an application. Scott Hanselman put together a nice list of .NET IOC containers.

Resolving a dependency generally is done by ‘wiring up’ or ‘binding’ dependencies via some form of configuration. You essentially tell the container, whenever an ‘IFileSystem’ type is requested, supply a concrete instance of type ‘FileSystem’. Many IOC containers also support auto-wiring (no configuration required) of concrete types.

The ‘wiring up’ process generally takes two forms: external or internal. External puts your configuration in a file (typically XML). Internal puts your configuration in code. They each have advantages. External allows you to re-configure without recompiling. Internal is generally more fluent and can leverage the state (what type of object requires the dependency for instance) to provide contextual binding.

The IOC container manages the creation and life time / life-cycle of the dependency. For instance, a dependency could be wired up so that only one instance is ever created. This is much like the singleton design pattern but without the testing issues (as seen in the ‘System.IO.File’ object). The following are the typical life-cycle options: a single instance used for every request, a new instance per request and new instance per lifetime of another object. The last option allows the lifetime of your dependency to be tied to a thread or an http session (for instance).

Why use an IOC Container?

This is a great question. Using an IOC container add another layer of complexity to your application. You can have DI without using an IOC container…and YOU SHOULD. Some may advocate to start every project using an IOC container. This definitely saves a refactoring step. The later you wait to introduce the CI container into the application the higher penalty you will pay in refactoring costs. Adding complexity is an engineering decision and depends upon your application. I prefer to add complexity only when necessary.

Here are a few benefits of an IOC container:

  • As an application gets larger we begin to feel the friction caused by managing the DI ourselves. This is especially true when you have dependencies that have dependencies that have dependencies. You can replace the complexity of the creational code with configuration / binding code for the IOC container.
  • Automated tests can have their own configuration / binding to allow fakes, mocks, and stubs to be used in lieu of actual class. This makes testing simpler.
  • The centralized configuration of the IOC container allows elements to be reconfigured easily. For instance replacing the concrete ‘FileSystem’ everywhere in the application only requires changing the binding to the new ‘IFileSystem’ implementation. The IOC container’s centralizes your configuration and makes your code easier to change.
  • Your code can remain free of any life-cycle management concerns. This allows the implementation of the ‘IFileSystem’ interface to avoid static members that are hard to test, or avoiding a caching system to fetch objects. The IOC container can manage this for you.
  • Many of the IOC containers leverage convention over configuration to provide features that enhance the experience. For instance Ninject has extensions that provide additional integration with other frameworks: asp.net webforms and asp.net mvc for instance.

Why not use an IOC Container?

There are reasons to avoid using an IOC container:

  • Using an IOC container adds complexity to your application. Small applications may not benefit from the added complexity. In these cases managing your dependencies manually is sufficient.
  • IOC containers add abstraction around the creation / life-cycle management process (yes I claimed this as a benefit). This abstraction requires a certain level of understanding for a developer working on the code. Developers are much more used to using ‘new’ to create objects than wiring up the IOC container. Once this barrier to entry has been overcome, then clarity is not really an issue.
  • There may be a slight performance hit using an IOC container to manager the creation / life-cycle of your dependencies. I have not done performance testing, but intuitively I can’t imagine this to be an issue with most applications.

 Additional Resources

The internet is full of information on DI and IOC. Here are a few resources on DI and IOC that you may find of interest:

Summary

This post was a brief introduction to dependency injection and inversion of control containers. In upcoming posts, I will dive into DI using Ninject 2.

No Responses

Leave a Reply

Your email address will not be published.

*