Dependency Injection: A Gentle Introduction

Published Mar 26, 2017
Dependency Injection:  A Gentle Introduction

Introduction: What Is Dependency Injection?

Dependency Injection is a software design principle where an object's dependencies are
provided to it, rather than the object creating its own dependencies.

dependencies-overview.jpg

I noticed that many of my students initially have trouble grasping the concepts and benefits of Dependency Injection, so I decided to write this article as a general introduction.

What is a Dependency?

In a nutshell, a dependency is something a class needs in order to do its work. Imagine an application that needs to log various activities. In that scenario, you would typically have a class whose responsibility is to handle logging. Classes that need to log things therefore have a dependency on the logging component.

What exactly does 'Injection' mean?

'Injection' just refers to the concept of a class getting its dependencies from an external source (often passed as constructor arguments) rather than creating them internally.

This approach is advantageous because it allows us to specify which specific implementation will be used in which cases.

Let's consider the logging scenario from above:

public class SomeClass(){
    ConsoleLogger logger;
    public SomeClass(){
        logger = new ConsoleLogger();        
    }

    public void DoSomeWork(){
        logger.Log("About to do some work.");
        // ...Do some work...
    }

    // ...Rest of class...
}

That doesn't look so bad...

True, this isn't the worst possible implementation, but it does have some subtle limitations:

  • The fact that SomeClass has a dependency on ConsoleLogger is not readily apparent unless you look at the source code.
    This is called a Hidden Dependency.
  • If, in the future, you want to change from using a ConsoleLogger to a (hypothetical) AzureQueueLogger, you have to edit SomeClass and any other classes that depend on ConsoleLogger to use the new AzureQueueLogger implementation. In a production application, this may mean editing every file in the entire solution.
  • Because SomeClass creates its own ConsoleLogger, you cannot write tests that verify SomeClass interacts with ConsoleLogger as expected.
    (i.e. verifying the correct message is logged, or even if anything is logged at all)
  • For the same reason, if you write unit tests for SomeClass, you will need a fully-functioning ConsoleLogger implementation, which means that you can't test SomeClass in isolation from the rest of the system.

Now let's refactor to inject our Logger dependency:

public class SomeClass(){
    ILogger logger;
    public SomeClass(ILogger logger){
        this.logger = logger;        
    }

    public void DoSomeWork(){
        logger.Log("About to do some work.");
        // ...Do some work...
    }

    // ...Rest of class...
}

Notice that instead of creating a Logger directly, we ask for it in the form of a constructor parameter. What that means is that anyone who wants to work with SomeClass must provide an ILogger for it to work with.

Another subtle difference is that we now use an ILogger interface instead of a concrete Logger class. ILogger is an abstraction that allows us to substitute any type that implements ILogger without changing the SomeClass code. Abstractions and Dependency Injection often go hand-in-hand.

So what's the Difference?

Functionally, there isn't one - SomeClass.DoSomeWork() operates identically in both examples, however the second example removes all of the limitations imposed by the first example:

  • It's explicitly clear that SomeClass requires an ILogger in order to do its work - in fact, you can't create one without it.
  • We can use SomeClass with any type that implements ILogger, instead of being limited to one specific implementation.
  • It is now possible to write tests that verify the correct interaction between SomeClass and ILogger.
  • Because we are using the ILogger abstraction, we can provide a mock ILogger when testing, enabling us to test SomeClass in complete isolation.

In a nutshell, we can see that Dependency Injection moves the responsibility for creating dependencies outside of the class that uses them.

Is There a Downside?

As with all tools, Dependency Injection has benefits and drawbacks.

One notable side effect of the Dependency Injection pattern is that as your application grows, so does your dependency list. This can rapidly lead to unwieldy constructors, for example:

public class HasManyDependencies{

    public HasManyDependencies(
        ILogger logger,
        ILocalRepository localRepository,
        IWebRepsitory webRepository,
        IMoonPhaseCalculator moonPhaseCalculator){
        // ... imagine the class does something 
        //useful with these dependencies 
        }
}

So if we want to create an instance of HasManyDependencies, our code will look something like this:

    var hasManyDependencies = new HasManyDependencies(
    new Logger(),
    new LocalRepository(),
    new WebRepository(),
    new MoonPhaseCalculator());

That's some fairly ugly code just to get an instance of HasManyDependencies, but it can be even worse - Imagine, for example, if LocalRepository, WebRepository, and MoonPhaseCalculator have dependencies of their own, the constructor call rapidly becomes painful to write.

The good news is that there are tools we can use to mitigate this constructor madness:

Inversion of Control (IoC) to the Rescue!

To solve the constructor complexity issue, we can use an Inversion of Control Container (IoC Container). The container takes on the responsibility of creating instances and fulfilling their dependencies in a way that greatly mitigates the impact of complex constructors.

"Inversion of Control" is a broadly-defined software engineering principle that refers to inverting the flow of control compared to traditional procedural programming. In the case of Dependency Injection, the inversion is related to dependency creation and lifecycle management.

Note: This post will only touch on the subject of IoC. For more information, check out Getting Started with Dependency Injection Using Castle Windsor.

In regular procedural programming, it is common to create instances using the new operator. When using an IoC container, we instead ask the container for an instance (called 'Resolving') and it gives us what we asked for. For example, instead of the mess of code above, our code would look something like this:

    // Assume that 'container' is a configured IoC container...
    var hasManyDependencies = container.Resolve<IHasManyDependencies>();

In the code above, when we call container.Resolve<IHasManyDependencies>, the container creates all of the dependencies required by HasManyDependencies (and any dependencies they may have themselves) and provides us with a ready-to-use HasManyDependencies instance.

The benefit to this approach is that we now have a class (the IoC container) that has the sole responsibility for creating and injecting dependencies, freeing us from the burden of complex constructors.

Conclusion:

Although Dependency Injection appears confusing on the surface, in reality it's just a slightly different pattern that provides multiple benefits to your code:

  • Loose coupling enables more flexible, composable software implementations and facilitates code reuse.
  • Vastly improves testability by enabling test suites to inject specialized test implementations instead of relying on a production stack.
  • When combined with Abstractions, enables us to swap implementations at runtime based on runtime conditions.
  • Works cleanly with IoC containers to mitigate inherent constructor complexity.

Dependency Injection also carries some indirect benefits, encouraging practices such as SRP (Single Responsibility Principle) and the Open-Closed Principle, leading to cleaner, more easily maintained code.

Like This? Check out my other CodeMentor articles!

Discover and read more posts from Ed Mays
get started
Enjoy this post?

Leave a like and comment for Ed

9
1