Dependency Injection
Goal
- Understand how dependency injection works.
- Learn when dependency injection should be used.
- Gain experience in using dependency injection with Google Guice.
Concepts
- constructor injection
- dependency
- dependency injection (DI)
- field injection
- DI container
- factory pattern
- inversion of control (IoC)
- service locator pattern
- setter injection
- singleton
- wiring
Library
com.google.inject.AbstractModule
com.google.inject.AbstractModule.bind(Class<T> clazz)
com.google.inject.Guice
com.google.inject.Guice.createInjector(Module... modules)
com.google.inject.Module
com.google.inject.Injector
com.google.inject.Injector.injectMembers(Object instance)
com.google.inject.binder.LinkedBindingBuilder
com.google.inject.binder.LinkedBindingBuilder.to(Class<? extends T> implementation)
com.google.inject.binder.LinkedBindingBuilder.toInstance(T instance)
javax.inject
javax.inject.Inject
javax.inject.Singleton
Dependencies
Lesson
To say that a project has a dependency simply means that it depends on something else in order to work. So far we've mostly been talking about library dependencies; in fact this very lesson indicates a dependency on a third-party library that needs to be included in your Maven POM. But we can also talk about the dependencies of a particular class implementation. To implement one interface, a class may need a reference to the implementation of another interface to delegate some of its responsibilities.
Think back to the FarmService
interface from the lesson on business logic. In addition to feeding the animals, the farm needs to collect eggs from the hens.
In order to collect eggs, the implementation class FarmServiceImpl
will need access to an implementation of the EggCollector
interface.
FarmServiceImpl
thus has a dependency on EggCollector
, and here the decision has been made to use a ByHandEggCollector
, which goes around to the hen houses, lifts up the chickens, and collects the eggs in a pail. But there also exists a ContraptionEggCollector
, which uses some combination of cranes, ropes, and pulleys to gather the eggs each day—perhaps something like the figure on the side.
On day we may want one day to change egg collection approach, but this is made difficult by having the ByHandEggCollector
implementation hard-coded into FarmServiceImpl
. If someone were to use our library without access to the source code, they would lose the flexibility to change the egg-gathering implementation. Perhaps most importantly, having this dependency directly referenced in the code makes it hard for us to test FarmServiceImpl
as a single unit, especially if its dependencies in turn have more dependencies.
Inversion of Control
One way to solve this problem is to add another layer of indirection using a technique called inversion of control (IoC). This is a general term that could apply to everything from event handling, callbacks, or simply delegating to another entity to provide a result. At its most fundamental level, IoC is an approach in which you hand over the normal sequence of control of your program logic to something else.
The normal sequence of control for creating an object in Java is to create it directly using the new
operator, as is done with ByHandEggCollector
above. This immediately calls the object's constructor and returns a new instance of the EggCollector
implementation, the one we specified by name. With inversion of control, we offload the responsibility of deciding which type of EggCollector
to create to some other object or framework.
Factory
One way to delegate the responsibilities of creating the EggCollector
is to use the factory pattern, which you've been familiar with for some time now. It could be as simple as single method in a separate class.
Service Locator
A variation of the factory design pattern is the service locator pattern. Because many services do things and don't maintain state information specific to one caller, one can make a service be a singleton so that only one service exists for all components of the program to use. (See Singletons below for more information.) A service locator provides a common location from which all parts of the program can come look up a desired service.
Like a factory, a service locator pattern can be swapped out to provide other implementations. But most commonly a service locator is configured with the service singletons it will make available. It may create a map of interfaces to implementations, for instance, which can be configured with specific implementations before the service locator is used.
Dependency Injection
Dependency injection (DI) is a form of inversion of control taken to the extreme. In many ways DI could be considered a variation of a service locator, but crucially the DI framework will actively provide or inject
the dependencies needed by a class.
JSR-330
For the DI framework to inject dependencies, your class must provide the DI framework some avenue of injecting the correct value. While DI frameworks have historically used proprietary approaches for specifying how your code will be injected, JSR 330 and the javax.inject
package now provide a set of standard annotations which most DI frameworks have adopted.
The javax.inject.Inject
annotation is used to indicate the point at which a dependency should be injected. Use of @Inject
will indicate to the DI framework that it should supply some needed implementation. (Exactly how this works is explained later in the lesson.)
Injection Types
There are several ways a dependency can be injected, and there is no requirement that you stick to one only (although consistency promotes understadability). Here we'll discuss the three most important approaches: field injection, setter injection, and constructor injection.
Field injection
Instance variables can be points of injection. This approach is most similar to the first FarmServiceImpl
variation above.
Note that a general interface EggCollector
is indicated as the field type. Simply by using the @Inject
annotation will cause the DI framework to magically
provide the correct implementation of EggCollector
—provided that the DI framework has been configured, as explained later in this lesson.
Setter Injection
Setter injection uses a JavaBean-type setter method to provide an entry point for injection. Rather than magically
setting the instance variable, the DI framework will magically
call the correct setter at the appropriate time.
Constructor Injection
With constructor injection, the dependencies are provided up-front as arguments to the constructor.
Singletons
If you would like the DI container to ensure that only one instance of your class is instantiated in the container, you can use the javax.inject.Singleton
annotation. In fact many services and managers are meant to serve various callers, and should be used as singletons. If you mark an implementation with @Singleton
, rather than creating a new instance of the implementation for each injection, the DI framework will create a single instance for injecting at all the appropriate injection points.
DI Containers
Until now we have ignored how to configure which implementations should be injected, as well as what exactly is doing the injection. Both of these matters are controlled by the DI container. One of the first libraries that made DI popular is the Spring Framework, which has now provides a plethora of interrelated modules and frameworks to handle everything from security to social networking. Google provides its own dependency injection framework named Guice.
Guice
Guice (which is pronounced like juice
) is the original dependency injection framework from Google. The Guice DI container will be used in these lessons, but most of the concepts appear in other DI containers as well.
Modules
Configuration of the Google Guice DI container takes place inside a com.google.inject.Module
instance. The com.google.inject.AbstractModule
class makes it easy to specify which concrete implementation should be used for a requested type, by passing the type (usually an interface) to AbstractModule.bind(Class<T> clazz)
. The bind()
method returns a LinkedBindingBuilder
instance which acts as a fluent interface for specifying which implementation should be used.
The most common binding uses LinkedBindingBuilder.to(Class<? extends T> implementation)
, specifying the type of the desired concrete implementation. Here is how we could configure a module to use a ContraptionEggCollector
for collecting eggs:
Injector
The actual DI container for Guice is the com.google.inject.Injector
interface. You create an injector using the main com.google.inject.Guice
class. Specify which module has the bindings you want and call the static factory method Guice.createInjector(Module... modules)
.
Once you have an implementation of a Guice Injector
, you can ask it for an implementation of a particular type. The magic happens behind the scenes: the injector will determine which concrete type is bound to the abstract type or interface requested and create it for you, automatically injecting dependencies at @Inject
points. As is often the case, the concrete type itself may have dependencies (specified as interfaces if following best practices). The injector will in term look up the concrete implementation for each of these dependencies, creating and injecting them in turn, and continuing to create bound dependencies as needed. The returned instance is therefore likely connected to an entire graph of dependency instances—bound by the module; and created and injected by the injector.
Scope
TODO https://github.com/google/guice/wiki/Scopes
Spring
TODO
TODO talk about named injection and @Named
Benefits and Drawbacks
Dependency injection, like indirection in general, provides flexibility to your code. Its benefits include the following:
- Consolidates the configuration of dependencies into one place.
- Facilitates testing by allowing different implementations to be swapped in.
- Allows separate implementations to be used for development than in production.
But as you learned at the beginning of this course, additional layers of indirection bring complexity and lower the transparency of the system. Dependency injection has several drawbacks:
- Adds a dependency to the dependency injection system itself. Only constructor injection relieves this dependency somewhat by allowing manual construction when desired.
- Obscures the configuration of relationships.
- Promotes abstraction to an almost opaque level. A business logic consisting of merely a
Thing
performing someAction
, in whichThing
andAction
must be wired with the DI container, may provide the ultimate in flexibility but will result in a confusing system that is hardly maintainable.
Review
Summary
TODO
Gotchas
- If you add new dependencies to your classes, don't forget to add them to the configuration.
- Don't forget to mark your services as singletons, or you may be surprised that information set by one caller cannot be found by another caller because independent instances of the service were created.
In the Real World
- Dependency injection is more appropriate for a few top-level managing objects such as repositories, managers, and services, not for domain objects or value objects.
- Field injection is inflexible and violates object-oriented principles. Use some other DI approach.
- If you absolutely must use dependency injection, try to use constructor dependency.
- If you are creating a library, don't pull in a particular DI framework as a dependency just to use the JSR-330 annotations. Declare a dependency to
javax.inject:javax.inject
and let the application that uses your library indicate which dependency injection to use. You may declare a specific dependency injection framework for testing.
Think About It
- TODO
Self Evaluation
- TODO
Task
Retrofit your main business logic classes in Booker so that they are configurable using dependency injection via Google Guice.
- Make all your business logic implementations singletons. This includes managers, services, and repositories.
- Use constructor injection in all instances.
- Just for practice, add setter injection to inject the
PublicationRepository
dependency into yourPublicationManager
implementation. Comment this out with a note that this is only an example and use constructor injection for the actual, functioning code! - You will need to get access to a Guice module. Make your application flexible by providing a
Booker
constructor that takes a Guice module as a parameter. You can configure the module in theBooker.main(…)
method. - Configure your
PublicationRepository
implementation to use aFilePublicationRepository
. Whichever repository used needed will need to be initialized via thePublicationRepository.initialize()
method. Because yourFilePublicationRepository
must be initialized with information specific to the application, configure an actual instance ofFilePublicationRepository
rather than its class. - Kick things off by requesting an instance of
BookerServices
from the DI container at the start of your application.
See Also
References
- Inversion of Control Containers and the Dependency Injection pattern (Martin Fowler)
- JSR 330: Dependency Injection for Java (Java Community Process)
Resources
Acknowledgments
- The New Multi-Movement Machine for Gathering Easter Eggs cartoon by William Heath Robinson (31 May 1872 – 13 September 1944) and now in the public domain.