This goes out to all the professional software developers out there working with time constraints just trying to get their job done. Dependency injection (DI) is one of those kinds of concepts that fall through the cracks and becomes assumed knowledge at some point. At a high level it means anything your classes depend on are passed in (usually via a constructor) and it helps make more testable code. Does that make sense? No. not even a little bit. That explanation is terrible.
If you are working in a project that has some level of DI going on (commonly Dagger or Guice in the JVM world) but are anywhere from slightly apprehensive to downright fearful of it, this will help take the edge off. Get it? Edges.. because there’s graphs.. wait, what?
What is Dependency Injection?
A pragmatic way to look at dependency injection is that you have two groups of classes in your programs. Things that depend on resources and things that do not. What’s a resource? Here’s a non-exhaustive set of examples:
- configuration files
- external caches
- a third party API
Why does that matter? When you test your code, you can have overly, and unnecessarily complex integration test setups, or you can pass in a class that represents the database. A class that responds the way the test needs it to to validate the specific condition it is testing. A different class allows you to run tests without ever hitting the internet or relying on some sketchy docker image that clones your external service.
Singletons are resources
The other big advantage? Object lifecycle when it comes to singletons. If you see tests that are littered with test-only methods that clear caches or reset long lived data structures, that means the code is not taking advantage of DI for rebuilding an object graph.
A singleton object is a resource like any of the previously mentioned ones, except it’s just managed internally in the same process as your program. They tend to either guard other resources or retain global data the same way an external cache layer would.
Singletons are also the main way developers use DI and then stop. Since the lifecycle of the singleton is managed by the DI framework, it tends to be a top level class when the program starts and can be set up without delving too deep into how the rest of DI stuff is intended to work.
What is the object graph?
At the most simple level, one could achieve dependency injection by passing an initialized database connection pool from the first function down through every other function until it gets to the lowest one on the call graph that needs it. Doing this for every resource possibly needed is brutal, and would result in function calls with a ridiculous amount of parameters. This is what a DI Framework (like Dagger) does for you behind the scenes. It silently makes this available to the classes that need it, and let you skip passing it for the ones that do not.
This requires a bit of a change in how you instantiate objects in general.
For example, let’s assume you had a Java program that would normally create an instance of a Frobulator, and that Frobulator internally requires a Frob (obviously, what else would require frobulation?)
With dependency injection the same class could look like this:
The common terminology for the difference in classes is Newable for classes instantiated the classic way and Injectable for classes instantiated via DI.
The object graph is the construction of all the dependencies required by the program and ensuring that they are defined somewhere enough for the program to function at runtime.
The previous example assumes that
Frob either has an empty constructor or another class that functions as a Provider.
A Provider (commonly grouped inside a class with the name Module) can really just be thought of as an external constructor. The benefit of this external constructor is that it can return subclasses and do some preprocessing on the objects being instantiated.
For example, if our program needs to frobulate, the implementation can provide a very specific instance. This example defines
Heater inline, but could do so in separate files.
Heater are not doing anything tricky with construction, using the
@Inject annotation on the constructor will create an implicit provider to save us some boilerplate code.
If the only dependencies for a
MetalFrob are also injectables, we could even rewrite the
MetalModule as follows:
The benefit for testing here is that now when a
Frob is needed in our tests, we can replace it with a slightly different object by creating a secondary module:
By merging this object graph with the main one used by the software, we can get more touch points into the software, with more direct control over how simulated resources respond.
At the end of the day, that’s really all a Module is. A definition of how to instantiate interfaces or how to override the construction of injected objects.
More than one instance
One way this tends to break down in practice is when we need to instantiate on demand as opposed to only during construction.
Take this newable class that has a
startFresh() method that will be called several times:
This runs into a problem that our previous example can’t solve. Luckily, the Provider comes to the rescue again.
With Dagger (other DI frameworks have similar concepts) the Provider is just another class that can be passed around, and is itself generated when the object graph is created at startup.
Parameterized construction is the second wrinkle that appears fairly quickly in a DI environment, and is discussed slightly less frequently. This occurs when there is an instance attribute that would be defined in the constructor if it were a newable class, but the constructor is not available for injectables.
Newable with instance parameters:
A simple approach to solve this for injectables would be to make a version of the class that requires the DI parameters only in the constructor, then other attributes are set with setter field accessors.
Here is a naive injectable without instance parameters:
This is problematic and should be avoided. The reason here is that
- It’s lame
- We can do better
- There’s a race condition that has now been introduced between construction and all of the required fields being set
In the naive example there is a period between when the object is instantiated and when
setMaxFrobulations() is called, if at all. This is something that a normal constructor solves for us that in the naive version creates complexity and reduces readability.
What we can do to work around this is to make the constructor private and apply a Factory pattern.
This allows the Factory to be constructed holding a reference to the
Frob passed in by the injector, and then calling the private constructor in the
create() method with any instance parameters.
There is one shortcoming of this design, the object graph is now broken. When it comes to object creation:
- injectables can create both other injectables and newables
- newables can only create other newables
This is due to the fact the injector that is quietly being passed around by the framework stops being passed at the newable boundary. Sometimes this is fine. If the classes you are creating genuinely do not need external resources or replaceable parts, DI may not provide you with much value. It is worth nothing the dependent code may change later, which create some extra work if care was not taken up front.
To support parameterization without breaking the object graph we can use the following factory pattern merging both previous examples:
In the preceding example we now have a
Factory.create() method that is an atomic action the same way our newable constructor was. Due to the way DI frameworks work, we can’t make the
LimitedFrobulator constructor private, but package-private is good enough.
In this definition, we have also defined the
LimitedFrobulator.Factory as a singleton simply by adding the
@Singleton annotation. This means we don’t need to allocate a new Factory every time. The choice of when to make the Factory a singleton or not is really defined by the use case. If we needed a fresh
Frob for every
Frobulator then a singleton does not help us.
Once more unto the breach
Now that we have laid out some basic implementations using DI, let’s develop a more realistic example.
For this program we will have an apartment simulator. The apartment simulator would need an instance of an apartment injected in.
As part of the apartment simulator, we need a
moveIn() operation. This will generate anywhere from 1-10 moving boxes
l have an apartment simulator. The apartment simulator would need an instance of an apartment injected in.
Any time you find a box in the universe, there is a high likelihood that there will be a cat who is already living inside it.
Since the simulator may want to adjust the chance of a cat being in the box, we will read that from an environment variable in the Provider.
This has the interesting effect that the object graph could have been broken here if we did not push an injected cat into the box. The other aspect worth noting is that depending we have hidden some logic inside the
ApartmentSimulatorModule that may make sense directly inside the injectable’s constructor. Be wary of doing this.
Now, as we all know, because they like to be punctual, all cats have pocket watches tucked away somewhere:
But what is this? The
pocketWatch needs to be synchronized, most likely to an NTP server.
This gives us a singleton NTP Server that can be replaced in unit tests to return a set time whenever the cats need to synchronize their pocket watches! Without dependency injection, we would have needed to pass this NTP server all the way down through each of the previous constructors just to get the same effect.