Dependency Injection in JavaScript

K - Oct 23 '17 - - Dev Community

Cover image by Papiertrümmer on Flickr

Why?

We all write code that depends on other code, this is completely normal. Even if we don't use any libraries, we will start to structure our codebase somehow. Maybe we modularize everything and now a module depends on another module, etc.

You probably heard that we should write code that is loosely coupled so we can replace parts of our software later, but what does this actually mean and how do we achieve this?

One way to do this is called Dependency Injection or short DI.

How?

DI boils down to one idea: remove explicit dependencies in the code and replace it with an indirection, but what does it mean when coding?

What is an explicit dependency?

You define some more or less static entities in your codebase. For example classes or functions.

class A {}

function f(x) { return x * x; }
Enter fullscreen mode Exit fullscreen mode

But, defining a class or a function doesn't make them explicit. The way they are used is the important factor here.

Their names can be used to reference them in other places of your code. For example, you could have a class B that uses class A.

class B {
  constructor() {
    this.a = new A();
  }
}
Enter fullscreen mode Exit fullscreen mode

Or you could call function f inside a function g that adds something to its result.

function g() {
  return f() + 10;
}
Enter fullscreen mode Exit fullscreen mode

And now the usage of function f and class A became explicit. Now class B only works if there is a class A defined and function g only works if there is a function f defined.

This may seem to many developers as a rather trivial fact and most of the time it has no greater implications, because the classes or functions never change.

But more often than not code changes and now the dependent code has to be re-written.

How to get rid of explicit dependencies?

The basic idea is, you don't call to functions or classes with their explicit names anymore. In statically typed languages this also means to get rid of the type annotations, but since JavaScript is dynamically typed, we just have to get rid of the class and function names.

Instead of writing

const a = new A();
Enter fullscreen mode Exit fullscreen mode

or

const result = A.someStaticMethod();
Enter fullscreen mode Exit fullscreen mode

You save a reference to A and pass this to the code that needs to call it. This allows you to change the reference to another class, when needed.

    class C {
      constructor(helperClass) {
        this.helper = new helperClass();
      }
    }
    ...
    let someHelperClass = A;
    ...
    if (someCondition) someHelperClass = B;
    ...
    const c = new C(someHelperClass);
Enter fullscreen mode Exit fullscreen mode

The same works with functions.

    function h(doSomething) {
      return doSomething() + 10;
    }
    ...
    let doSomething = f;
    ...
    if (someCondition) doSomething = g;
    ...
    const result = h(doSomething);
Enter fullscreen mode Exit fullscreen mode

The condition can come anywhere. Some DI frameworks even configure them via config files.

You can also create your objects and inject them instead of the reference to the class.

    class C {
      constructor(helper) {
        this.helper = helper;
      }
    }
    ...
    let someHelperClass = A;
    ...
    if (someCondition) someHelperClass = B;
    ...
    const c = new C(new someHelperClass());
Enter fullscreen mode Exit fullscreen mode

Practical Examples

You have a software that gets some data from services. You got multiple classes, each for one service, but they all share the same interface. Now you can create a condition via command line arguments, config file or environment variables that decides which class to use.

    class ServiceA { getData() {} }
    class ServiceB { getData() {} }
    class ServiceC { getData() {} }

    let Service;
    switch(process.env.APP_SERVICE) {
      case 'serviceB':
        Service = ServiceB;
      break;
      case 'serviceC':
        Service = ServiceC;
      break;
      default:
        Service = ServiceA;
    }
    ...
    class Application {
      constructor(Service) {
        this.service = new Service();
        this.run = this.run.bind(this);
      }
      run() {
        this.service.getData();
      }
    }
    ...
    const myApplication = new Application(Service);
    myApplication.run();
Enter fullscreen mode Exit fullscreen mode

You have UI components that render other UI components nested inside them. You could let them decide what child components the parent use like that

    const planets = ["mercury", "venus", "earth", "mars"];

    function List(planets) {
      return "<someMarkup>" + planets.map(planet => Item(planet)) + "</someMarkup>";
    }
    ...
    const markup = List(planets);
Enter fullscreen mode Exit fullscreen mode

Or you could simply pass the finished children into the parent

    function List(children) {
      return "<someMarkup>" + children + "</someMarkup>";
    }
    ...
    const markup(data.map(item => Item(item)))
Enter fullscreen mode Exit fullscreen mode

Now the parent can use whatever children you give it.

    const children = [FirstItem("Planets")]
    data.forEach(planet => children.push(Item(planet)));
    List(children);
Enter fullscreen mode Exit fullscreen mode

As you can see here, you don't have to pass a reference to the class or function as reference to get DI benefits. You can also create your results or instances before injecting it into the target code that depends on it.

Problems

Sometimes this gets a bit out of hand. Often DI is used rather simple. Like I said, when you pass a reference to a class or a function in you code around it's already DI, but you can take take things further by using external config files that decide what code is used, so the users have a way to modify the software without re-writing any code.

If you overdo it, you end up with big configurations and nobody really nows anymore what code is really running in the end.

Conclusion

Dependency Injection is a way of structuring code so it becomes more loosely coupled. It can be used in small parts of the application or govern the whole workings of it.

But as with everything, use in moderation. The more explicit the code, the easier it is to reason about it.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .