viphat / til

Today I Learned
http://notes.viphat.work
0 stars 1 forks source link

Dependency Injection in Javascript #271

Open viphat opened 6 years ago

viphat commented 6 years ago

Source

Dependency Injection (DI) is one of the foundations of good OOP practice.

What is Dependency Injection

Dependency Injection is a 25-dollar term for a 5-cent concept.

Dependency Injection is a software design pattern that deals with how components get hold of their dependencies.

There are only three ways a component (object or function) can get a hold of its dependencies:

  1. The component can create the dependency, typically using the new operator.
  2. The component can look up the dependency, by referring to a global variable.
  3. The component can have the dependency passed to it where it is needed.

The first two options hard-code the dependency to the component when creating or looking up the dependency - this is not the most favorable solution as it makes it virtually impossible to modify the dependencies. This is a problem in tests, where it is often advantageous to provide mock dependencies for test isolation.

The third option is the most workable, since it removes the responsibility of locating the dependency from the component. The dependency is simply handed to the component.

DI was used so classes in Java would not be tightly coupled together, for example:

import java.util.logging.Logger;
public class MyClass {
  private final static ILogger logger;
  public MyClass(ILogger logger) {
    this.logger = logger;
    // write an info log message
    logger.info("This is a log message.")
  }
}

In the above overused example, the logger is passed into the MyClass constructor rather than it being constructed by MyClass. In classic Java, 'functions' have to be contained in a Class as methods and you must instantiate a new object based on the class before you can use the method.

The only way to decouple a dependency between objects is to instantiate the dependency outside of the dependent object and pass it into the constructor of that object.

An IoC container or a Service Locator has the responsibility of finding and instantiating these dependencies.

Implementations of DI in Javascript

From Angular 1:

class SomeClass {
  constructor(greeter) {
    this.greeter = greeter;
  }
  doSomething(name) {
    this.greeter(name);
  }
}

In the above example SomeClass is not concerned with creating or locating the greeter dependency, it is simply handed the greeter when it is instantiated.

This is desirable, but it puts the responsibility of getting hold of the dependency on the code that constructs SomeClass.

To manage the responsibility of dependency creation, each Angular application has an injector. The injector is a service locator that is responsible for construction and lookup of dependencies.

This is acceptable in an OOP world where removing the burden of 'new-ing' classes is beneficial.

The downside is that the dependencies are just strings in Angular 1 and mistyping (and even uglify-ing code) breaks the DI system.

Angular 2 solves this problem at compile time, using TypeScript interfaces.

When to use Dependency Injection

Just like any pattern it can become an anti-pattern when overused, and used incorrectly. If you are never going to be injecting a different dependency why are you using a dependency injector?

Dependency injection is effective in these situations:

  1. You need to inject configuration data into one or more components.
  2. You need to inject the same dependency into multiple components.
  3. You need to inject different implementations of the same dependency.
  4. You need to inject the same implementation in different configurations.
  5. You need some of the services provided by the container.

Dependency injection is not effective if:

If you know you will never change the implementation or configuration of some dependency, there is no benefit in using dependency injection.

Alternative methods of decoupling code

Higher order functions

Higher order functions can accept functions as parameters or a function as a return value

function foo(bar, func) {
  return func(bar);
}

Module System

import foo from ‘Foo’;
export default function(bar) {
  return foo(bar);
}

Currying and composing functions

function curried($foo) {
    return function ($bar) use ($foo) {
        return $bar + $foo;
    }
}
const add10 = curried(10);
add10(5); // returns 15
add10(8); // returns 18
// func uppercase…
// func scoldName…
const shoutyTellOff = compose(uppercase, scoldName);
shoutyTellOff(‘roland’); // returns ‘STOP IT, ROLAND!’

Composition is a technique where small pure functions, with a single responsibility, are joined together to create a new function which has the functionality of the composed functions

Conclusions

While Dependency Injection frameworks are worthwhile in Static Languages, such as Java, it is not in dynamic languages such as JavaScript (and arguably PHP) and even less so in functional languages, such as Clojure or Haskell.

If your language supports higher-order functions (and if it supports currying, even better) and you have a functional mindset there is no reason to use DI frameworks.

Dependency Injection, the design pattern, in the case of a static class-based language, is a good pattern to implement and the underlying premise of DI (to pass in dependencies) is fundamental to good software engineering.

DI, the design pattern, is used to circumvent the deficiencies of the language — which is what design patterns intrinsically are.

If your language supports the feature you need “out of the box” then there is no need to implement a Design Pattern, but that’s a different blog post for another time…