DEV Community

loading...
Cover image for What is dependency injection?

What is dependency injection?

nnadozie profile image Nnadozie Okeke Updated on ・6 min read
  • What it is, an overview.
  • Identifying when to use dependency injection.
  • How do I use it?
  • Why you should use IoC containers.
  • When not to use DI.

What is Dependency Injection, an overview.

It's an approach used to keep dependencies and their consumers loosely coupled from one another, and therefore allow for easier unit testing, as well as replacement of software modules with minimal code changes.

Essentially, rather than instantiating dependencies within consumers, we instantiate them outside the consumers and access them within the consumers by passing them to the consumers as parameters.

We do this,

consumer(dependency d) {
   use dependency
}

new consumer(new dependency())
Enter fullscreen mode Exit fullscreen mode

rather than this,

consumer() {
    dependency d = new dependency();
    use d;
}
Enter fullscreen mode Exit fullscreen mode

For the record, consumers can also be called high-level modules, and their dependencies: low-level modules.

When implemented correctly, the approach allows us to adhere to the Dependency Inversion Principle created by Robert Martins, which states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions. source

Example

Now, when I first saw the DI principle it didn't make any sense to me, so let's look at an example with a short trip to Middle Earth, using the same example found in this repository of Java design patterns.

Galdalf ship of smoke

Example statement:

Gandalf, the old wizard, likes to fill his pipe and smoke tobacco once in a while. However, he doesn't want to depend on a single tobacco flavor only but likes to be able to enjoy them all interchangeably.

In OOP (Object Oriented Programming) terms, the old wizard object consumes the tobacco object and calls on it when smoking.

Simple enough right? We can implement them this way:

The tobacco class

    public class Tobacco {

        public void smoke(Wizard wizard) {
            System.out.printf(
                "%s smoking %s",
                wizard.getClass().getSimpleName(),
                this.getClass().getSimpleName());
        }
    }
Enter fullscreen mode Exit fullscreen mode

The wizard class

    public class Wizard {
        private Tobacco tobacco;

        public Wizard() {
            Tobacco tobacco = new Tobacco();
            this.tobacco = tobacco;
        }

        public void smoke() {
            this.tobacco.smoke(this);
        }

        public Tobacco getTobacco() {
            return this.tobacco;
        }
    }
Enter fullscreen mode Exit fullscreen mode

And use them like so,

Wizard gandalf = new Wizard();
gandalf.smoke();
Enter fullscreen mode Exit fullscreen mode

You can play around with a runnable code snippet here

Unfortunately, this is actually a bad way to implement what we want, and I'll cover why in the next section.

Identifying when to use dependency injection.

Okay, Gandalf's done having plain Tobacco. He wants SecondBreakfastTobacco.

Let's understand when to apply DI by looking at how it makes our job of giving Gandalf different flavored tobacco much easier,

especially when compared to our first approach.

In the process, we'll get the tests in this runnable code snippet passing.

What's wrong with our first approach?

In order to give gandalf what he wants we'd need to do something like this:

gandalf = new WizardWithSecondBreakfastTobacco();
gandalf.smoke();
Enter fullscreen mode Exit fullscreen mode

Do you see the problem?

Golum thinking

We just needed an entirely new WizardWithSecondBreakfastTobacco class with duplicate code 😰😲, or

if instead of writing new classes we wrote new constructors, we would need complex constructors for each tobacco flavor, like so:

    /*
      constructor which expects a string
      indicating what tobacco class to create.
    */

    public Wizard(String flavour) {
        if(flavor equals "SecondBreakfastTobacco")){
            create SecondBreakfastTobacco
        }
        else if ... create other tobacco class instances
    }
Enter fullscreen mode Exit fullscreen mode

Not to mention we'd also need a tobacco class for each flavor of tobacco with duplicate smoke methods, like so:

public class SecondBreakfastTobacco {

        public void smoke(Wizard wizard) {
            System.out.printf(
                "%s smoking %s",
                wizard.getClass().getSimpleName(),
                this.getClass().getSimpleName());
        }
    }
Enter fullscreen mode Exit fullscreen mode

That's a lot of logic and duplicate code we could avoid by picking up on tell-tale signs that we can use dependency injection.

Signs that tell us we can use dependency injection

frodo's glowing sword

Jacob Jenkov does an excellent job of spelling out good situations for wielding our DI pattern:

  • You need to inject configuration data into one or more components.
  • You need to inject the same dependency into multiple components.
  • You need to inject different implementations of the same dependency.
  • You need to inject the same implementation in different configurations.
  • You need some of the services provided by the container.

And the third one rings true for our use case, where we need to give Gandalf different flavors of tobacco. That is, we need to give the Wizard class different implementations of the Tobacco class. So let's whip out our DI pattern.

How to use dependency injection?

We simply apply the Inversion of control principles to use dependency injection, starting with:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.

In simple terms, Gandalf should not know how to make tobacco to smoke tobacco. Instead, Gandalf's existence should depend on wherever Wizards come from, and tobacco's existence should depend on wherever tobaccos come from.

Personally, I try to imagine a Wizard factory which has a wizard blueprint, and a tobacco factory which has a tobacco blueprint/recipe, and then I start with the blueprints.

Let's code that out.

First we make our Tobacco blueprint

public abstract class Tobacco {

  public void smoke(Wizard wizard) {
            System.out.printf(
                "%s smoking %s",
                wizard.getClass().getSimpleName(),
                this.getClass().getSimpleName());
        }
}
Enter fullscreen mode Exit fullscreen mode

Note how this is an abstract class, which only exists as a template for other classes to extend from.

Great. Now we'll make our Wizard blueprint and move to the next principle

public interface Wizard {

  void smoke();
}
Enter fullscreen mode Exit fullscreen mode

2: Abstractions should not depend on details. Details should depend on abstractions.

In simple terms, a tobacco blueprint/recipe should not depend on an existing tobacco. We need the blueprint first, before we create tobaccos based on the blueprint.

And very importantly, whenever we want to consume a tobacco, we should simply expect something of type tobacco blueprint, so that we're free to consume any flavor of tobacco we want. See here for another explanation

So let's make all the tobacco flavors we want using the tobacco blueprint

public class SecondBreakfastTobacco extends Tobacco {
}

public class RivendellTobacco extends Tobacco {
}

public class OldTobyTobacco extends Tobacco {
}
Enter fullscreen mode Exit fullscreen mode

And finally we'll make a Wizard based on our Wizard blueprint

public class AdvancedWizard implements Wizard {

  private final Tobacco tobacco;

  public AdvancedWizard(Tobacco tobacco) {
    this.tobacco = tobacco;
  }

  @Override
  public void smoke() {
    this.tobacco.smoke(this);
  }

  public Tobacco getTobacco() {
    return this.tobacco;
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice how AdvancedWizard, in its constructor, depends on the blueprint Tobacco, instead of any of the particular Tobacco implementations.

And we're done!

  • AdvancedWizard no longer needs to know how to make tobacco, it get's given tobacco in it's constructor.
  • Both AdvancedWizard and all the tobacco classes now depend on abstractions.

To use them, we can do this:

 AdvancedWizard gandalf = new AdvancedWizard(
new SecondBreakfastTobacco()
);
 gandalf.smoke();
Enter fullscreen mode Exit fullscreen mode

And when Gandalf wants RivendellTobacco, we can now simply do this:

gandalf = new AdvancedWizard(
new RivendellTobacco()
);
gandalf.smoke();
Enter fullscreen mode Exit fullscreen mode

But there're some issues to be aware of which make it important to use IoC containers.

Why you should use IoC Containers

For just the two entities in our example, Wizard and Tobacco, let's call them W and T, we know that when we need a concrete Wizard, we first need a concrete Tobacco. So we can say W depends on T, such that when we need W, we first create T.

But if you're reading this, you're probably building applications with way more than just two entities.

So what if we had a situation, A depends on W depends on T? Now every time we need A, we first create W which requires that we first create T.

This situation can easily blow up into a long chain of dependencies, say A, W, T, B, D, Q, R. How in Rivendell, you may be asking, are we going to create R, then Q, then D, then B, then T, then W, each time we want to use A? That's ridiculous!

I also think it is, and that's why IoC containers exist.

IoC containers exist to keep track of our objects and the inter-dependencies between them, and they take care of injecting the dependencies for us,

So we don't have to worry about doing these ourselves.

Popular frameworks, such as Spring, Dot Net, Angular, Laravel, all come with IoC containers, and there're even standalone IoC containers such as Inversify for JavaScript, or Pico Container and Google Guice for Java.

When not to use DI.

As explained in this stackoverflow discussion, you probably don't want to use DI if:

  • your interface (blueprint) is more likely to change than your implementation.
  • it is logical to encapsulate logic in a dependency, but that logic is the only such implementation of a dependency.
  • you don't really have a reason for using DI, except just wanting to.

Discussion (0)

pic
Editor guide