What is dependency injection, what is Dagger and how can it come in handy for us to write cleaner and easier to test code.
Disclaimer from the translator. This translation was made for the purpose of self-education, and on Habré is laid out on the assumption that many novice Android developers, who, like me, did not have the opportunity to be born as a fifth-generation Java-speaking developer, find it difficult to understand the end products of multi-year layers of concepts and development methods . This series of articles is an excellent example of how to explain complex things, and I hope you will like it as much as I do. About all the errors and inaccuracies noted, please report to the PM.Dependency injection (DI) is a great technique that makes it easy to cover an application with tests, and Dagger 2 is one of the most popular Java / Android frameworks designed for this purpose. Moreover, most of the introductory courses on Dagger 2 are based on the assumption that the reader is already well acquainted with DI and its rather complicated terminology, which makes it difficult for newcomers to enter.
In this series of articles, I will try to provide you with a more friendly introduction to Dagger 2 with many examples of ready-to-compile code.
In order not to spread the idea of the tree, I deliberately leave behind the history of DI frameworks, exactly like their countless varieties. We begin with the introduction to the class constructor, and look at the remaining options as we go.
')
So, what is dependency injection?
Dependency injection is a technique that simplifies testing and reusing classes. Let's look at an example of how it can be applied. Suppose we need to write an application that prints the current weather conditions to the console. The simplest implementation would look something like this:
public class WeatherReporter { private final WeatherService weatherService; private final LocationManager locationManager; public WeatherReporter() { weatherService = new WeatherService(); locationManager = new LocationManager(); } public void report() {
Some methods are intentionally omitted because they are unimportant in this case. Note that
WeatherReporter needs two objects to perform its own work: the
LocationManager , which determines the user's location, and the
WeatherService , which
outputs the temperature at the specified coordinates.
In a well-designed object-oriented application, only a small share of responsibilities lies on each object, the rest of the work is delegated to other objects. These other objects are called
dependencies . Before an object starts doing any real work, all its dependencies must be somehow
resolved . For example, dependencies are resolved for our
WeatherReporter by creating new instances of these objects in the constructor.
For small applications, dependency initialization in the class constructor works quite well, but as the application grows, this approach reveals a number of drawbacks. First, it makes the class less flexible. For example, if the application should be multiplatform, we may need to replace our current
LocationManager with another one, but this will not be so easy. We may want to use the same
LocationManager in several places, but this will be difficult to do until we change the class.
Secondly, our class does not give in to isolated testing. Creating one
WeatherReporter object entails creating two other objects, which ultimately fall under testing along with the original object. This can be a serious problem if one of the dependencies depends on a costly external resource (an Internet connection, for example) or it itself has a large number of dependencies.
The root of evil in this case is that our class performs two different duties. He is obliged to know not only
how to perform his task, but also
where to find the components necessary for its implementation. If, instead, we provide the object with everything needed to do its work, the problem will disappear. It also facilitates the interaction of the class with other components of the application, not to mention the simplification of testing.
public class WeatherReporter { private final WeatherService weatherService; private final LocationManager locationManager; public WeatherReporter(WeatherService weatherService, LocationManager locationManager) { this.weatherService = weatherService; this.locationManager = locationManager; } public void report() {
This approach is called
dependency injection . In an application that relies on DI, objects do not have to "scour around" in search of dependencies or create them themselves. All dependencies that are provided to them (
implemented ) are ready for use.
Graphing
Of course, at some point someone must initialize all the dependencies and provide them to those objects that need them. This step, referred to as
dependency graph building , is usually performed at the point of entry to the application. In a desktop application, for example, this code is located inside the
main method, as in the example below. In an Android application, this can be done inside the
onCreate activity method.
public class Application { public static void main(String args[]) { WeatherService ws = new WeatherService(); LocationManager lm = new LocationManager(); WeatherReporter reporter = new WeatherReporter(ws, lm); reporter.report(); } }
If the project is simple, as in our previous example, initialization and implementation of several dependencies in the
main method are justified. However, most projects consist of many classes with their dependencies that must be resolved. Initializing and binding all of this together requires writing a lot of code. Even worse, this code will be changed regularly when adding a new class to the application or adding a new dependency to an existing class.
To illustrate this problem, let's turn our example into a more realistic one. In practice, the
WeatherService class will need, say, a
WebSocket to communicate with the network.
LocationManager will need a
GPSProvider to interact with the hardware. Among other things, most classes will need a
Logger to display debug information in the console. The modified
main method now looks like this:
public class Application { public static void main(String args[]) { Logger logger = new Logger(); WebSocket socket = new WebSocket(); GPSProvider gps = new GPSProvider(); WeatherService ws = new WeatherService(logger, socket); LocationManager lm = new LocationManager(logger, gps); WeatherReporter reporter = new WeatherReporter(logger, ws, lm); reporter.report(); } }
Very quickly, the entry point to our application began to swell from the abundance of initialization code. To create the only
WeatherReporter object we really need, we have to manually initialize many other objects. As the application grows and grows into classes, the
main method will also continue to swell until one day it becomes completely unsupported.
How can Dagger 2 help?
Dagger 2 is an open source tool that generates most of the initialization code for us, based on just a few annotations. When using Dagger 2, the entry point to our application can be written in just a few lines of code, no matter how many classes we have and how many dependencies are present. Below is a new
main method for our example using Dagger.
public class Application { public static void main(String args[]) { AppComponent component = DaggerAppComponent.create(); WeatherReporter reporter = component.getWeatherReporter(); reporter.report(); } }
Note that we are not required to write code to resolve dependencies or indicate how these dependencies are intertwined. This has already been done for us. The
DaggerAppComponent class, automatically generated during project compilation, is smart enough to know that the
WeatherReporter class needs a
Logger ,
LocationManager, and
WeatherService , which in turn needs
GPSProvider and
WebSocket . When the
getWeatherReporter method is
called, it will create all these objects in the correct sequence, create connections between them, and return only what we needed.
In order for Dagger to work, we need to complete a few steps. First, add annotation to the constructor of each class that Dagger should be aware of. Below is an example of how to do this for one particular class, but it is also necessary to annotate all classes that need some dependencies or which themselves act as dependencies for other classes.
public class GPSProvider { @Inject public GPSProvider() {
If you are worried about the contamination of the application with Dagger-specific code, you will be pleased to know that the
Inject annotation
is standardized (JSR 330) and many other tools work with it, like Spring or Guice. Accordingly, switching to another DI framework will not require changing the set of classes in the application.
In the next step, we need to create an interface with the
Component annotation, which declares the methods that will return the objects we need. It is not necessary to declare methods for each class in our project here, only methods for classes directly used at the application entry point are required. In our example, the
main method needs only
WeatherReporter , so we declare only one method in the interface.
@Component public interface AppComponent { WeatherReporter getWeatherReporter(); }
All that's left is to integrate Dagger with our build system. When using Gradle, simply add new dependencies to
build.gradle .
plugins { id "net.ltgt.apt" version "0.7" } dependencies { apt 'com.google.dagger:dagger-compiler:2.6' compile 'com.google.dagger:dagger:2.6' }
Everything, the project can be compiled and executed. If you want to try to do it yourself, the source code for our example is
available on Github .
As a conclusion. Please note: we can still use any class without the Dagger, as before. For example, we can still initialize the
Logger manually using the
new operator, for example, in a unit test. Dagger does not change the behavior of the language, it simply generates convenient classes that initialize objects for us.
In the next article in this series, we will see what happens when the same dependency can be used in multiple classes or when we cannot annotate a class for some reason.
What to read
If dependency injection seems like a useless technique, or you want to see more examples when DI simplifies testing, take a look at Miško Hevery, Russ Ruffer and Jonathan Wolter’s amazing
Writing Testable Code (
pdf ).
If you are more interested in the theory of introducing dependencies and all its varieties, I recommend the essay
Inversion of Control Containers and the Dependency Injection Pattern (translated into Russian -
Part 1 ,
Part 2 ) by Martin Fowler.
To be continued ...
Original article