Dependency Injection 102 -Instrumentation with Guice

AJ Ribeiro
Bigeye
Published in
6 min readJul 14, 2020

--

[Part 1 of this series, Part 3 of this series]

In the first post in this series, we discussed the fundamentals of the Dependency Injection (DI) pattern.

Although some benefits were immediately apparent, there is still some room for improvement. After implementing the DI pattern, we are left with a lot of boilerplate code in our main app for creating all the dependencies of our House class.

Imagine the dependency graph of our code as a tree:

What we do in our code is start building dependencies at the bottom of the tree and work our way up until eventually we have everything we need to build a House:

This work is very formulaic, and tooling exists to do all of that formulaic work for us.

DI Tooling

The two modern libraries for DI in Java are Guice (pronounced “juice”) and Dagger. Guice was developed by folks at Google, while the original version of Dagger was developed by engineers at Square, and the most recent version of Dagger is an iteration which comes out of Google.

I will focus on Guice for this post because it’s the framework that I’m more familiar with, but most of what is written will also be applicable to Dagger because they have similar APIs. I will compare Guice and Dagger in Part 3 of this series.

For detailed instructions on how to instrument Guice, you can check out Baeldung.

Introduction to Guice

The main selling point of Guice (and Dagger) is that we don’t have to manually create our dependencies anymore. The tooling will do that for us if we instrument our code with Guice’s API. There are a few patterns for how to use Guice: Field Injection, Constructor Injection, and the Provider Pattern. Here we will discuss each of the patterns briefly.

Guice Patterns

Field Injection

Field injection in the context of DI means that we annotate the fields of a class with the @Inject annotation, which tells Guice to inject the dependency directly into the class. We can also then remove them from the constructor, since we won’t be calling the constructor at all. In fact, we can delete the no-arg constructor altogether because Guice doesn’t need it. For example, the Bathroom class could look like this:

Notice the @Named annotations on the Sink dependency. If you recall from Part 1, Sink is an interface which has two implementations: KitchenSink and BathroomSink. The way that we tell Guice how to inject Implementations of interfaces is with Bindings or Providers. These will be discussed later.

A full working example of our app using field injection can be found on GitHub, specifically in src/main/java/di/guice/fieldinjection.

Constructor Injection

Construction injection means that instead of telling Guice what to inject, we instead annotate the constructors to tell Guice how to build our objects. Starting at the bottom of our dependency tree, Guice will start using the annotated constructors to instantiate objects, which will be injected into the constructors of classes further up the tree. For example, our Kitchen class could look like this:

Notice that here instead of @Inject-ing the individual fields, we are @Inject-ing the constructor, which tells Guice how to build our class. If Guice knows how to build all of the classes that we need, we never need to build anything manually.

The working example can be found in src/main/java/di/guice/constuctorinjection.

Provider Pattern

The Provider Pattern is similar to Constructor Injection in that we tell Guice how to build our objects. The difference is that instead of annotating a constructor and having Guice implicitly figure out how to build objects, we explicitly tell Guice how to build certain classes by writing providers. For example, a provider method for the Kitchen class could look like this:

When we write providers, Guice will use those methods to instantiate the provided object. Guice will also try to determine how to create the parameters of the provider method by looking for either no-arg constructors, @Inject-annotated constructors, or other providers. The key is that we need to tell Guice how to make everything we care about. These providers go into Module classes which will be discussed in the next section.

In the scope of our simple app, the Provider Pattern only complicates things, but in a more complicated application it can be extremely useful. For example, if you need a third party dependency to be injectable, unless it’s already instrumented with Guice annotations, you will need to write a provider to tell Guice how to instantiate it.

The working provider pattern example can be found in src/main/java/di/guice/providerpattern.

Modules and Bindings

Modules and bindings are special tools which Guice uses to inject implementations of interfaces. To create a module, what we need to do is create a class which extends the abstract class AbstractModule. Within this class, we need to override the configure method, in which we can bind interfaces to implementations. In the example of our app, the Module could look like this:

In plain English what the first binding says is to inject the KitchenSink implementation for any Sink dependency which is annotated with @Named(“Kitchen”). The second binding says inject the BathroomSink implementation of Sink for any Sink dependency which is annotated with the @Named(“Bathroom”) annotation.

Note that if we use the Provider Patten (see above), we can use a provider as an alternative to the binding. For example, we can write a provider like this for KitchenSink:

This tells Guice that wherever it sees a dependency for a Sink interface named “Kitchen”, to use this provider to build it.

Singletons

Guice allows us to easily leverage singletons as well. If we decide that we want our KitchenSink instance to be a singleton, we can add a method call to the binding in our Module:

This binding is the same as before, except that we have added .in(Singleton.class). This tells Guice to make the KitchenSink a singleton. We can also achieve this behavior by adding an annotation to a provider:

Putting it all together

Now that we have told Guice what to inject and how to bind interfaces to implementations, we can put it all together in our application to get an instance of House. In order to do this, we will need to create an injector, let it know about AppModule, and then ask for an instance of House. The code for this is:

And voila, we have an instance of House in our main application with the boilerplate from before gone.

What does DI tooling do for me?

As discussed in Part 1, the DI pattern alone provides separation of concerns, configurability, and ease of testing. DI tooling provides a few added quality-of-life improvements on top of that.

First, as we have seen in the examples above, we remove a lot of the boilerplate code which was present in the main application in Part 1 of this series.

Second, it speeds up development. Consider a situation where I want to add a dependency to a class, maybe I want to add a Fridge to Kitchen. With Guice, I am able to simply add the Fridge field to the class with an @Inject annotation and I will have it. Without DI tooling, I would have to find every place where an instance of Kitchen is constructed, build an instance of Fridge, and add it to the Kitchen constructor call.

DI tooling removes nearly all friction from adding dependencies to classes.

What’s next?

In Part 3 of this series, we will compare and contrast Guice and Dagger, as well as discuss some of the implementation details for folks who are more interested in the nuts and bolts of the tooling.

This code is available to clone from GitHub.

--

--