Dependency Injection

Delegation implies dependency

With delegation as our approach for distillation we discover a curious problem.

We introduce a dependency relationship from the distilled to the delegate.

Where Do Dependencies Come From?

There are three primary ways our components can get their dependencies:

  • Construction
  • Service Location
  • Dependency Injection

One of these is not like the other two. Can you guess which?

Construction

With construction, we instantiate the dependency directly ourselves. Not only do we take on providing our own dependency, but all of its constructor arguments too.

class Foo {
    constructor() {
        // ...

        this.bar = new Bar(a, b)
    }

    // ...
}

Service Location

Service location is any mechanism for reaching out to a parent scope in order to reference the dependency. This might be in the form of an import or a module level local.

import { bar } from "./bar"

class Foo {
    constructor() {
        this.bar = bar
    }
}

Dependency Injection

“Dependency Injection” means it is up to someone else to provide our dependencies to us. Usually this is in the form of function or constructor arguments.

class Foo {
    constructor(bar) {
        this.bar = bar
    }
}

Why Dependency Injection is Different

Dependency injection is the only way to obtain dependencies that embodies the principle of Inversion of Control.

This is a general idea wherein we relinquish control over objects, data, or other portions of our program to well… someone else!

In the case of dependency injection, we are relinquishing control over where our dependencies come from and which dependencies we’ll use.

Someone else must “inject” those dependencies into us, usually via function or constructor arguments.

💡 With construction and service location, in order to change our
dependencies, we must modify the component to refer to different
dependencies. Regressions incoming!

With dependency injection, the dependencies can change without modifying the consumer because it’s up to “someone else” to pick and provide them.

SOLID Principles

Dependency injection is the D in SOLID.

It helps us achieve each of the other SOLID principles.

Single Responsibility Principle

By delegating implementation details out to subordinate encapsulations we leave only orchestrating those dependencies as the sole responsibility of most components.

class Foo {
    someMethod(name) {
        const thing = getTheThing(name)
        const cleanThing = cleanAThing(thing)
        saveAThing(thing)
    }
}

Open / Closed Principle

By changing which dependencies are injected, we can change the behavior of the consumer without modifying it.

const logger = 
    process.env.DEBUG
        ? console.log
        : () => { return }

const foo = new Foo(logger)

Liskov Substitution Principle

By specifying our dependencies as interfaces, we don’t care which specific dependency we get, we just need one we can work with.

class Foo {
    constructor(bar: Barlike) {
        this.bar = bar // any Barlike will do!
    }
}

Interface Segregation Principle

Taking in narrowly typed dependencies that represent just the functionality we need avoids utilizing resources or behavior our function or class doesn’t actually need, or shouldn’t use.

type Health = {
    maxHp: number
    currentHp: number
}

type Attack = {
    weapon: Weapon
    strength: number
}

class Entity {
    maxHp: number
    currentHp: number
    weapon: Weapon
    strength: strength
}

function applyDamage(health: Health, attack: Attack) {
    health.currentHp -= attack.weapon.damage * attack.strength
}

const player: Entity
const enemy: Entity

applyDamage(player, enemy)

Clarity of Mind

The biggest advantage to dependency injection:

💡 Focus and clarity of mind while working on any individual component.

If we understand the responsibility or purpose of the component we’re working on, we can start to formulate a high-level strategy it might take.

As you start to write the component, we no longer need to start off by typing out all the implementation details.

Instead, we can simply declare the dependencies, even if they don’t exist yet, that would be the most helpful delegates for saying what our component’s strategy is.

All we need to worry about is orchestrating those dependencies to carry out that strategy in the most distilled and expressive way possible.

Once we are satisfied, we can move on to implementing the dependencies and hashing out the implementation details.

Of course, we can apply this exact same thinking to the implementation of those dependencies.

We can keep delegating until there’s nothing left to delegate; Until further delegation would result in simply saying “1. draw the owl”. At that point it’s time to stop delegation and actually implement the narrow concern or responsibility at hand.

function drawTheOwl() {
  doTheOwlDrawing()
}

Summary

In this chapter we explored:

  • How components can obtain their dependencies
  • Inversion of control
  • How dependency injection helps us invert control
  • How dependency injection helps us write SOLID code
  • The way we are able to focus on one component at a time

Dependency injection is a pattern that lets us fully apply our design process of distillation through delegation.

In the next chapter, we’ll investigate who is ultimately responsible for the instantiation of objects within our program