Dependency Injection in Java
The article was initially published at cchacin.github.io
UPDATE: Editorial changes to improve the readability, thanks to
Shefali Agarwal
.UPDATE 2019–12–19: Editorial changes to improve the readability.
Java is an object-oriented language with some functional aspects included in its core. Like any other object-oriented language, classes and objects are the foundations of any functionality that we can write and use. The relationships between the classes/objects make it possible to extend and reuse functionality. However, the way that we choose to build those relationships determine how modular, decoupled, and reusable our codebase is, not only in terms of our production code but also in our test suites.
In this article, we are going to describe the concept of Dependency Injection in Java and how it helps us have a more modular and decoupled codebase, which makes our lives easier, even for testing, without the need of any sophisticated container or framework.
When a class ClassA
uses any method of another class ClassB
, we can say that ClassB
is a dependency of ClassA
.
In this example, ClassA
is calculating 10% of the value, and calculating that value, it's reusing the functionality exposed by ClassB
.
And it can be used like this:
Now, there is a big problem with this approach:
If we needed to change/replace ClassB
with ClassC
because ClassC
has an optimized version of the calculate()
method, we need to recompile ClassA
because we don't have a way to change that dependency, it's hardcoded inside of ClassA
.
The Dependency Injection Principle
The Dependency Injection Principle is nothing but being able to pass (inject
) the dependencies when required instead of initializing the dependencies inside of the recipient class.
Decouple the construction of your classes from the construction of your classes’ dependencies
Setter Injection (Not recommended)
With this approach, we remove the new
keyword from our ClassA
. Thus, we move the responsibility for the creation of ClassB
away from ClassA
.
ClassA
still has a hard dependency on ClassB
but now it can be injected
from the outside:
The above example is better than the initial approach because now we can inject
in ClassA
an instance of ClassB
or even better, a subclass of ClassB
:
But there is a significant problem with the Setter Injection
approach:
We are hiding the ClassB
dependency in ClassA
because by reading the constructor signature, we cannot identify its dependencies right away. The code below causes a NullPointerException
on runtime:
In a statically typed language like Java, it’s always a good thing to let the compiler help us. See Constructor Injection
Constructor Injection (Highly recommended)
ClassA
still has a hard dependency on ClassB
but now it can be injected
from the outside using the constructor:
ADVANTAGES:
- The functionality remains intact compared with the
Setter Injection
approach - We removed the
new
initialization from theClassA
. - We still can inject a specialized subclass of
ClassB
toClassA
. - Now the compiler is going to ask us for the dependencies that we need in compile time.
Field Injection (Kids don’t try this at home)
There is a 3rd way to inject dependencies in Java, and it is called Field Injection
. The only way for field injection to work is:
- Mutating the field because it’s a non-private and non-final field
- Mutating a final/private field using reflection
This approach has the same problems exposed by the Setter Injection
approach and additionally adds complexity due to the mutation/reflection required. Unfortunately, this is a pretty common pattern when people use a Dependency Injection Framework
.
NOTE:
When a class
ClassA
uses any method of another classClassB
we can say thatClassB
is a dependency ofClassA
.If
ClassA
has a dependency onClassB
,ClassA
constructor should requireClassB
.
Realistic Example
Every single Hello World
example for any idea, concept, or pattern is super simple to understand, and it just works fine. But when we need to implement it in a real project, things get more complicated, and often, as engineers, we tend to try to solve the problem by introducing new layers to the problem instead of understanding what the real problem is.
Now that we know the advantages of the Dependency Injection Principle
using the Constructor Injection
approach, let's create a more realistic example to see some inconveniences and how can we solve it without introducing a new layer to the mix.
The Todo’s Application
Let’s design a Todo’s Application to perform CRUD operations (Create, Read, Update, Delete) to manage our todo list, and an original architecture can be like this:
TodoApp
is the main class that is going to initialize our application; this can be an android app, web page, or a desktop application using any framework.TodoView
is the class that would display a view to interact with, this class is going to delegate the data-related aspects to theTodoHttpClient
. It's only responsibility is to paint/draw/render the information and get the input to perform actions against the data using theTodoHttpClient
dependency.TodoHttpClient
is the class that contains a set of HTTP methods to persistsTodo
objects using a REST API.Todo
is a value object that represents a todo item in our data store.
Let’s write the Java classes for our design using the Constructor Injection
approach that we just learned:
Now let’s focus our attention on the relationship between the TodoView
and TodoHttpClient
classes and add more details to them:
Testing our design
Let’s create a unit test for the TodoView
class where we test the class in isolation without instantiating any of its dependencies. In this case, the dependency is TodoHttpClient
:
Now that we have our test case passing, let’s analyze how our design impacts the testing approach:
- We introduced the Mockito framework to be able to create a fake instance of
TodoHttpClient
, and that adds much complexity. - We have to prepare our instance of
TodoHttpClient
to fake the return of an empty list when calling thegetAll()
method, now our unit test also contains implementation details about theTodoHttpClient
. - Additionally, since
TodoHttpClient
is a concrete class, we cannot change the implementation to call a DB instead without having to change theTodoView
class as well, and we would need to rewrite the unit tests even when they should isolate this implementation detail.
Let’s improve our design
One thing that we can do to decouple our classes is to introduce an interface since the Java language is always a good thing to rely on abstractions instead of relying on actual implementations.
Let’s put an interface between TodoView
and TodoHttpClient
:
TodoProvider
Let’s make the TodoHttpClient
to implement that interface:
Now the TodoView
class looks like this:
What do we gain with these changes?
We are able to change the TodoHttpClient
with something like TodoDBProvider
in the TodoApp
and the application behavior would remain the same:
Let’s see how that helps in unit tests
The test is still green which is great, but wait… nothing changed actually 😦
The only changes were related to naming:
TodoHttpClient
->TodoProvider
no value for the test.httpClient
->provider
no value for the test here.- We are still relying on the mocking framework.
- We are still coupling the test to the interface’s name:
TodoProvider
. - We are still coupling the test to the method name:
getAll()
Can we remove the mocking framework?
Sweet, now our design is more flexible since we can inject a different TodoProvider
implementation, and we can do the same in our tests without using a mocking framework. But, we are paying the price: Verbosity, the mocking framework removes the need for implementing every single method from the interfaces.
Only the beginning
In the next article, let’s remove the verbosity from the tests and write an even better design.
Stay tuned for more posts like this.
Originally published at https://cchacin.github.io.