PUBLIC OBJECT

A Dependency Injector’s 3 Jobs

You can do dependency injection (DI) manually or with a library.

Constructing your application’s dependency graph by hand is a cute exercise but not practical beyond toy examples. You’ll eventually find yourself extracting the repetitive manual DI code into your own bespoke library, one that’s likely to be undesigned and incomplete.

But before you can do DI with a proper library, you’ve got to pick one. In this post I’ll describe the duties of a dependency injector. This could help you to understand the tradeoffs in various libraries. It may also explain why DI advocates like myself care so strongly about seemingly small differences.

Job 1: Uniform Syntax

A DI library’s user interface is its syntax: how to declare dependencies and how to satisfying them.

The javax.inject package is a popular mechanism to declare dependencies. Here’s what it looks like in a chess app:

class MoveRecommender {

  @Inject
  public MoveRecommender(
      BoardRanker boardRanker,
      PredictionPruner predictionPruner) {
    ...
  }
}

The @Inject annotation signals that each constructor parameter is a dependency. This annotation is useful for the library and for other developers!

Each DI library has syntax for defining rules on how dependencies are satisfied. Here’s Guice 1.0’s fluent DSL:

class SimpleChessBotModule extends AbstractModule {
  @Override public void configure() {
    bind(BoardRanker.class).to(PieceCounter.class);
    bind(PredictionPruner).to(FixedDepthPredictionPruner.class);
  }
}

Having uniform syntax for declaring and satisfying dependencies is great! It isolates business problems from dependency plumbing. Instead of making a tedious decision every time you consume or satisfy a dependency, you pick a DI library once and that decision is made forever.

Job 2: Modularity

DI separates the code that needs dependencies from the code that satisfies ’em. The net effect is that we can express service classes as a dependency graph:

Each dependency-declaring class is a node in the graph. Dependencies specify which other nodes the class node should connect to.

Each dependency-satisfying rule adds edges to the graph. Each edge tells the DI library how to satisfy a particular dependency requirement.

The DI library merely builds the graph from our specification. We can adjust the graph by adjusting the specification. We could vary the graph for development vs. production. The development one offers more logging, more metrics, and perhaps even a debug drawer.

It should be easy to share individual classes and large modules between projects. We could build an Android app and a chess server that share common subgraphs. For testing we should be able to compose test-specific graphs.

Modularity is valuable even if you aren’t sharing code between client and server. It lets you split a big application into small, standalone modules. This is great for teams, fast builds, and agility. Wanna iterate on your feature in isolation? Modularity is your friend.

For true encapsulation and composability the DI library should support multiple graphs in use concurrently. Imagine the bot dependency graph has one ExecutorService and the chat graph has another. Deploying these features in the same application should not cause them to share threads!

two graphs side-by-side

Job 3: Safety

I like typed languages. A good compiler means I can make big changes without taking big risks.

DI libraries have responsibilities similar to a compiler’s type checker. They should confirm that all the application’s dependencies can be satisfied. Problems should be reported with an actionable error message. Here’s how Guice does it:

com.google.inject.CreationException: Unable to create injector, see the following errors:

1) No implementation for BoardRanker was bound.
  while locating BoardRanker
    for the 1st parameter of MoveRecommender.<init>(MoveRecommender:25)
  at BasicChessBotModule.configure(BasicChessBotModule.configure:17)

2) No implementation for PredictionPruner was bound.
  while locating PredictionPruner
    for the 2nd parameter of MoveRecommender.<init>(MoveRecommender:25)
  at BasicChessBotModule.configure(BasicChessBotModule.configure:17)

2 errors

Detecting problems early is better. Good DI libraries detect at either compile-time or application start. If problems aren’t detected until dependencies are needed then making code changes becomes risky.

Plumbing Matters

The code that connects classes to their dependencies is the application’s plumbing. Good plumbing is invisible; bad plumbing makes everything shitty.

When you‘re picking a dependency injection library, don’t be seduced by syntax alone. Modularity and safety have a larger impact on how your application grows.