Architecture

An abstract take on the dependency injection pattern

This article will take a relatively abstract look at the design pattern called dependency injection (or inversion of control). I feel that most articles about dependency injection get too bogged down in the particulars of whatever example is being used to demonstrate the structure. In this article, we’ll present pure abstraction.

Well, maybe not pure abstraction- we do have to pick a particular programming language, after all! We will use Java in this article. If you don’t know Java, don’t worry too much. We’ll stick to “basic” Java- nothing esoteric.

A typical dependency situation

Consider the following dependency situation, in which a class Cls depends both upon an interface Intf and on an implementation Impl of that interface.

public interface Intf { void helperMethod(Object args); }
​
public class Impl extends Intf
{
    @Override
    public void helperMethod(Object args) { /* implementation */ }
}
​
public class Cls
{
    public void method(Object args)
    {
        Intf intf = new Impl();
        intf.helperMethod(args);
    }
}

Cls depends on Impl because it requires knowledge of the Impl type in order to execute new Impl(). To restate, our current dependency situation is:

Cls –creates–> Intf, Impl

Impl –is–> Intf

We want to be in a dependency situation in which Cls depends only on Intf and not on Impl. I.e., we want to decouple the implementation of Cls from any particular implementation of Intf.

A better dependency situation

To improve our dependency situation, we will pass the responsibility of creating Impl to some Container class that manages Cls. When commanded to do so, Container will “inject” a newly created  Impl instance into Cls by using Cls‘s public with-arguments constructor. (This means that Cls does actually have to have a public with-arguments constructor). This setup is called dependency injection. Implementing this dependency injection setup places us in the following much improved dependency situation:

Cls –has–> Intf

Container –has–> Cls

Container –has–> Intf

Container –creates–> Impl

Impl –is–> Intf

Now, Cls depends only on Intf and not on Impl, as desired.

In addition to solving the decoupling problem, dependency injection comes with a benefit so extravagant that most consider it to be “the purpose” of dependency injection. The benefit is this: in the case that it makes sense for instances of different classes to rely on the same Intf implementation (e.g. different classes could rely on the same database access object), Container can “inject” a single Intf instance into each class. In other words, our new dependency situation also allows for the sharing of one Intf instance between class instances.

Summary

To summarize, here are the two main benefits of dependency injection:

  1. Dependency injection decouples the implementations of classes from the implementations of those classes’ dependencies.

  2. In dependency injection, a “container” class manages the dependencies of all other classes. (So, those “other classes” do not use new statements to create their own dependencies). This allows for the sharing of a single dependency between many classes, when applicable.

Inversion of control

Since, in dependency injection, Container, rather than Cls, controls what and when Cls‘s dependencies are injected, control has in some sense been inverted. Dependency injection is thus an example of inversion of control. For this reason, a class such as Container is often referred to as the inversion of control container, or IoC container.

Note that while dependency injection is an example of inversion of control, not all inversion of control is dependency injection. This article by Martin Fowler details other examples of inversion of control.

Code

Here’s code that implements the dependency injection pattern.

public interface Intf { void helperMethod(Object args); }
​
public class Impl extends Intf
{
    private Object args;
    public Impl(Object args) { this.args = args; }
​
    @Override
    public void helperMethod() { /* implementation that uses this.args */ }
}
​
public class Cls
{
    private intf;
    public Cls(Intf intf) { this.intf = intf; }

    public void method() { intf.helperMethod(); }
}
​
public class Container{ /* implementation will be kind of complicated */ }
   
public class Main
{
    /* A config file typically performs performs the task of this method. */
    private Container configureContainer()
    {
       Object args = ... // get the arguments that should be passed to the Impl constructor
       Container cntr = new Container();
       
       /* The below line tells cntr all the information it needs to execute the statement
       "Intf intf = Impl(args)". */
       cntr.registerComponentImplementation(Intf.class, Impl.class, args);
       
       /* This next line tells cntr all the information it needs to execute the statement
       "Cls cls = new Cls(intf)". */
       cntr.registerComponentImplementation(Cls.class);
       return cntr;
    }
   
    public static void main(String[] _args)
    {
       /* This is how we call cls.method(). */
       Container cntr = configureContainer();
       Cls cls = (Cls) cntr.getComponentInstance(Cls.class);
       cls.method(); // This executes the same task as "cls.method(args)" did in the original situation.
    }
}

Extra: did we give Container unnecessary information?

One particular about the lines involving cntr.registerComponentImplementation() wasn’t immediately clear to me, and might be confusing to you, too. My question was: is it necessary to pass Intf.class to the first call of registerComponentImplementation()? It seems that there should exist an implementation of Container such that if we execute the following, cntr does what we would expect behind the scenes.

cntr.registerComponentImplementation(Impl.class, args);
cntr.registerComponentImplementation(Cls.class);

That is, it seems that cntr would have enough information to do new Cls(new Impl(args)). This is because cntr has a Cls, and Cls knows that Impl is an Intf. And, for the sake of argument, even if we assume that cntr somehow didn’t know about this through Cls, the JVM itself knows that Impl is an Intf– after all, executing new Cls(new Impl(args)) doesn’t require that we type cast new Impl(args) to Intf!

Answer: upon investigation, I found that it is just convention to pass more information than is strictly necessary in certain dependency injection frameworks.

Source

The structure of the Main class in the above derives from an example given in Martin Fowler’s article on dependency injection.

About the Author

Ross Grogan-Kaylor is an Associate Technical Consultant at Perficient’s Minneapolis office. He enjoys engaging with structural patterns in the syntax and in the high-level ideas of software development.

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up