What is Dependency Injection? – Part 3
This is the third in a series of blog posts that I’m putting together for developers who are beginning to work with Spring. This post attempts to describe the basics of what dependency injection is.
Dependency injection is a fundamental part of Spring. It is so core to Spring that it warrants some explanation.
In the past couple of posts including this one I haven’t even shown a single line of ‘Spring’ code. Dependency injection, by itself, does not require Spring. In fact, there are many frameworks (outside and within Java) that use Dependency Injection. This is because DI is a design pattern. It’s a way to structure your code and manage your dependencies. It helps you control how classes interact with (and depend on) each other, both in production code as well as and especially in your test code.
Let’s start of with an example. You can follow along on my Spring Beginner Tutorial on github.
In the example below, ‘ExampleRunner’ depends on ‘InterfaceDependency’:
How does interfaceDependency get instantiated and set on ExampleRunner?
The simplest way that it could get instantiated and set is via the constructor of ExampleRunner. We could ‘new’ it up in the constructor of ExampleRunner, like so:
But this creates a problem. ExampleRunner is now coupled to a concrete implementation, InterfaceDependencyImpl, which is a concrete class (i.e. not an interface). This is a problem because every place ExampleRunner is used, it is always going to use InterfaceDependencyImpl. They are coupled together, inseparable, and there’s nothing a client of ExampleRunner can do about that.
One place where coupling becomes a problem is when we want to unit test ExampleRunner. What if, for example, InterfaceDependencyImpl made a network call, or talked to a database, or performed some other long running operation. Now, anytime we run our unit tests against ExampleRunner we must wait for the network call to return (long and brittle), or depend on a database server to be running (unneeded dependency), or wait for some other process to finish. This is not good.
Dependency Injection to the rescue!
We can solve this problem with Dependency Injection (DI for short). DI follows the dependency inversion principle, which states:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
In our example above, ExampleRunner needs remove its dependency on InterfaceDependencyImpl. But how to we do that?
We do that by introducing the idea of an Injector.
An Injector is an object that coordinates dependencies. To keep things simple, the Injector in this example is the main method. Its job is to instantiate and set, or ‘inject’ dependencies.
Types of Injection
There are two different ways that dependencies are injected: Constructor Injection and Setter Injection.
The first method of injecting dependencies that I’ll describe is Constructor Injection. Take a look at the example below. This is code that exists in the Injector (main method in this example):
Here, we instantiate InterfaceDependencyImpl. We then pass it in as a constructor argument to ExampleRunner. The passing in as an argument to the constructor is the ‘injection’ part. This is Constructor Injection since the point we are injecting it into is the constructor.
If you run this code, you’ll see the following printed out:
Setter injection is done via a method, typically a setter method. In the example below, InterfaceDependencyImpl is passed into ExampleRunner via the setDependency method. You may have noticed that I don’t pass in any arguments to the constructor in this example — the dependency gets set via the setter:
Constructor injection and Setter injection are the most common ways to inject dependencies. I personally prefer constructor injection for all dependencies, as I prefer to have all my dependencies in place before a class is used. For me, I like knowing that the class has everything it needs before it’s used. However, there are cases where setter injection is appropriate. For example, sometimes you can avoid circular dependencies if you set the problematic property via setter injection (though you should avoid circular dependencies where possible anyway). In my current project, I can’t think of a single place where we use setter injection, and it’s a good-sized project.
What were you saying about testing?
Back to the testing discussion. If you remember, I stated that when two classes are too tightly coupled, testing becomes more difficult (and sometimes impossible). Being able to specify the dependencies of a class helps in this regard. For example, if I am writing tests for ExampleRunner, and I need to simulate specific behavior of InterfaceDependencyImpl (for example, throw an exception), it becomes problematic if ExampleRunner itself instantiates InterfaceDependencyImpl. It’s a problem because ExampleRunner nor my unit test can control whether InterfaceDependencyImpl throws an exception or not. There’s no reliable way to simulate that. If ExampleRunner controls the instantiation and setting of this dependency, I have no choice but to test with that implementation. However, if I use DI to set my dependencies, I do have control of the dependencies of the class I’m testing. So, if I wanted to simulate an exception, I can create a test class InterfaceDependencyWithExceptionImpl, which throws an exception when invoked. I then inject this implementation for the interfaceDependency property (via either Setter or Constructor injection, it doesn’t matter). My test code can then focus on the behavior of the unit under test (ExampleRunner in this case), and how it reacts to scenarios like a dependency throwing an exception. This way I can set up my tests in a way that simulates a wider variety of scenarios.
Dependency Injection is fundamental to understand when learning Spring. Furthermore, it promotes good design by encouraging components to be loosely coupled. DI promotes separating the creation of a class’s dependencies from the behavior of those classes, and opens up numerous testing scenarios. In these examples I purposely left Spring out, as my intention here is to show you simple examples of Dependency Injection so that you’re not bogged down with syntax and Spring-specific details.