Automatic Dependency Injection

Hear me out :P

In the composition root, instances of the objects in our program are created in an order determined by their dependency relations. When writing the composition root, we must work out this ordering. And of course this code will also need to be maintained as the structure of our program changes over time.

Don’t despair, there’s a generalized solution for automating all this :)

DI Container Basics

A Dependency Injection Container is a software pattern that allows us to avoid having to manually write our program’s composition root.

What is a DI Container?

Fundamentally a DI Container just a fancy key-value map. Yes, really!

💡 A DI Container is a key-value map:
    “a kind of thing” ⇒ “a way of making that kind of thing”

A DI Container maintains this mapping from “kind of thing” to “ways of making that kind of thing”.

How is the DI Container configured?

DI Containers typically utilize the builder pattern for creating associations:

const container = new Container()
container
 .bind(Foo)
 .to(Foo)

In this case, we associate Foo , a kind of thing, with the Foo constructor, a way of making them.

DI Containers will usually have many ways of specifying mechanisms for construction.

In this example, we give the container an arrow-function to call on each request:

container
 .bind(Foo)
 .toDynamicValue(() => new Foo())

As a final example, we could tell the container to always return the same statically constructed instance:

container
 .bind(Foo)
 .toConstantValue(new Foo())

What is a DI Container for?

After binding some things into the container, we can then ask the container for instances of those things:

const foo = container.get(Foo)

In this case, we have supplied the Foo class as the “kind of thing” we are interested in.

What we get back will depend on how we bound Foo into the container:

container.bind(Foo).to(Foo)
const foo1 = container.get(Foo) // new Foo constructed
const foo2 = container.get(Foo) // new Foo constructed
foo1 === foo2 // false!

Alternatively:

container.bind(Foo).toConstantValue(new Foo())
const foo1 = container.get(Foo) // Foo constructed above
const foo2 = container.get(Foo) // save Foo constructed above
foo1 === foo2 // true!

Recursive Dependency Resolution

So far, the DI Container may seem a little silly. Why use this pattern just to avoid calling new ourselves?

In all of the examples above, Foo didn’t have any dependencies of its own.

However, if we reintroduce the dependency graph from the last chapter we can see the DI Container’s real trick:

App -> Foo -> Bar -> Baz
                \ -> Boz

Let’s tell the container about these types:

container.bind(App).to(App)
container.bind(Foo).to(Foo)
container.bind(Bar).to(Bar)
container.bind(Baz).to(Baz)
container.bind(Boz).to(Boz)
const app = container.get(App)
app.start()

This is not much different than our original composition root:

const baz = new Baz()
const boz = new Boz()
const bar = new Bar(baz, boz)
const foo = new Foo(bar)
const app = new App(foo)
app.start()

However, there are some important key differences!

Ordering Doesn’t Matter

The order of the binding statements don’t matter. They could be reordered arbitrarily.

The container doesn’t need to be told about the topological order because it figures it out automatically.

We Don’t Need To Know As Much

In the composition root, we need to have intimate knowledge of all our types and their constructor parameters.

We also have to work out the order in which they must be composed. Now we don’t need to know any of those things.

We Can Skip Binding By Using Decorators

Later on we’ll learn that we can slap decorators on our types to automate binding the container:

@provide(Foo)
class Foo { ... }

How does the container work?

When we ask the container for an instance of App the container follows a process like this:

graph TB
 Get{Ask for App} --> Lookup[Do I have it?]
 Lookup -- yes --> Method[Which mechanism?]
 Lookup -- no --> Error
 Method -- const --> Const[Return const value]
 Method -- lambda --> Lambda[Return lambda return-value]
 Method -- constructor --> Constructor[For each dependency]
 Constructor -- Unresolved dependency --> Lookup
 Constructor -- All dependencies resolved --> Call[Call constru
 Call --> Return[Return constructed instance]

Summary

In this chapter, we learned:

  • A DI Container is basically a fancy key-value map
  • How to tell the container about our components
  • That we can ask the container for instances of our components
  • The container recursively satisfies the dependencies of our components
  • Using a container reduces the upfront and maintenance burden compared to a manual composition-root

Next Chapter

🍶 CH6: DI Container Patterns

In the next chapter we’ll take a look at some advanced DI Container concepts including how we can use it to make good on the architectural ideas we learned in earlier chapters.