@PascalPrecht #jsconfbp

Dependency Injection

For future generations

By @PascalPrecht
JSConf Budapest May, 2015

This is me That's my soap bubble gun

Let's talk about dependency injection!

"Dependency Injection
...as design pattern
...as framework."

DI as a design pattern

a.k.a. the 5-cent concept

class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor(engine, tires, doors) {
    this.engine = engine;
    this.tires = tires;
    this.doors = doors;
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
class Car {
  constructor(engine, tires, doors) {
    this.engine = engine;
    this.tires = tires;
    this.doors = doors;
    this.milesDriven = 0;
  }

  drive(miles) {
    this.milesDriven = this.milesDriven + miles;
  }
}
var car = new Car(
  new Engine(),
  new Tires(),
  new Doors()
);
var mockCar = new Car(
  new MockEngine(),
  new MockTires(),
  new MockDoors()
);

And that's it.

function main() {
  var engine = new Engine();
  var tires = new Tires();
  var doors = new Doors();
  var car = new Car(engine, tires, doors);

  car.drive(10);
}
function main() {
  var engine = new Engine();
  var tires = new Tires();
  var doors = new Doors();
  var car = new Car(engine, tires, doors);

  car.drive(10);
}
function main() {
  var yetAnother = new YetAnotherDependency();
  var engine = new Engine(yetAnother);
  var tires = new Tires(yetAnother);
  var doors = new Doors(yetAnother);
  var car = new Car(engine, tires, doors, yetAnother);

  car.drive(10);
}
function main() {
  var yetAnother = new YetAnotherDependency();
  var engine = new Engine(yetAnother);
  var tires = new Tires(yetAnother);
  var doors = new Doors(yetAnother);
  var car = new Car(engine, tires, doors, yetAnother);

  car.drive(10);
}

DI as a framework

function main() {
  var yetAnother = new YetAnotherDependency();
  var engine = new Engine(yetAnother);
  var tires = new Tires(yetAnother);
  var doors = new Doors(yetAnother);
  var car = new Car(engine, tires, doors, yetAnother);

  car.drive(10);
}
function main() {
  var injector = new Injector();
  var car = injector.get(Car);
  car.drive(10);
}

DI in Angular 1.x

Car.$inject = ['Engine', 'Tires', 'Doors'];
          
class Car {
  constructor(engine, tires, doors) {
    ...
  }
}

DI in Angular 1.x

Car.$inject = ['Engine', 'Tires', 'Doors'];
          
class Car {
  constructor(engine, tires, doors) {
    ...
  }
}

DI in Angular 1.x

var app = angular.module('myApp', []);

app.service('Car', Car);

app.service('OtherService', function (Car) {
  ...        
});

DI in Angular 1.x

var app = angular.module('myApp', []);

app.service('Car', Car);

app.service('OtherService', function (Car) {
  ...        
});

DI in Angular 1.x

var app = angular.module('myApp', []);

app.service('Car', Car);

app.service('OtherService', function (Car) {
  ...        
});

We still have some problems though:

  • Internal cache - Dependencies are served as Singletons
  • Synchronous by default - Async only possible through hacks
  • Namespace collision - A token is available in app only once
  • Built into the framework - Not possible to use without framework

Taking DI to the next level

DI in Angular 2

import { Injector } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
          
var car = injector.get(Car);
import { Injector } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
          
var car = injector.get(Car);
import { Injector } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
          
var car = injector.get(Car);


class Car {
  constructor(
    engine,
    tires,
    doors
  ) {
    ...
  }
}
import { Inject } from 'angular2/di';

class Car {
  constructor(
    @Inject(Engine) engine,
    @Inject(Tires) tires,
    @Inject(Doors) doors
  ) {
    ...
  }
}
import { Inject } from 'angular2/di';

class Car {
  constructor(
    @Inject(Engine) engine,
    @Inject(Tires) tires,
    @Inject(Doors) doors
  ) {
    ...
  }
}

Decorators are just functions

function Inject(dependencies) {
  return function (target) {
    target.parameters = dependencies;
  };
}

// so what basically happens is...
Inject(Engine)(Car)

Here's an article about that →

import { Inject } from 'angular2/di';

class Car {
  constructor(
    @Inject(Engine) engine,
    @Inject(Tires) tires,
    @Inject(Doors) doors
  ) {
    ...
  }
}
function Car(engine, tires, doors) {
  ...
}

Car.parameters = [
  [Engine],
  [Tires],
  [Doors]
];
import { Injector } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
          
var car = injector.get(Car);
import { Injector } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
          
var car = injector.get(Car);
import { Injector, bind } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(Engine),
  bind(Tires).toClass(Tires),
  bind(Doors).toClass(Doors)
]);
          
var car = injector.get(Car);
import { Injector, bind } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(Engine),
  bind(Tires).toClass(Tires),
  bind(Doors).toClass(Doors)
]);
          
var car = injector.get(Car);
import { Injector, bind } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(Engine),
  bind(Tires).toClass(Tires),
  bind(Doors).toClass(Doors)
]);
          
var car = injector.get(Car);
import { Injector, bind } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(OtherEngine),
  bind(Tires).toClass(Tires),
  bind(Doors).toClass(Doors)
]);
          
var car = injector.get(Car);
import { Injector, bind } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(OtherEngine),
  bind(Tires).toClass(Tires),
  bind(Doors).toClass(Doors)
]);
          
var car = injector.get(Car);

More Binding Instructions

.toValue()

bind(String).toValue('Hello World!')

expect(injector.get(String))
  .toEqual('Hello World!')

.toAlias()

class Engine {}
class V8 {}

bind(Engine).toClass(Engine)
bind(V8).toAlias(Engine)

.toFactory()

bind(Engine).toFactory(() => {
  if (IS_V8) {
    return new V8Engine();
  } else {
    return new V6Engine();
  }
})

Of course, a factory can have dependencies

.toFactory()

bind(Engine).toFactory((dep1, dep2) => {
  if (IS_V8) {
    return new V8Engine();
  } else {
    return new Engine();
  }
}, [Token, Token2])

Of course, a factory can have dependencies

.toFactory()

bind(Engine).toFactory((dep1, dep2) => {
  if (IS_V8) {
    return new V8Engine();
  } else {
    return new Engine();
  }
}, [Token, Token2])

Of course, a factory can have dependencies

What about asynchronicity?

.toAsyncFactory()

bind(Engine).toAsyncFactory(() => {
  return new Promise((resolve) => {
    fetch('/engineData.json')
      .then((data) => {
        resolve(new Engine(data));
      });
  });
})

.toAsyncFactory()

bind(Engine).toAsyncFactory(() => {
  return new Promise((resolve) => {
    fetch('/engineData.json')
      .then((data) => {
        resolve(new Engine(data));
      });
  });
})

.toAsyncFactory()

bind(Engine).toAsyncFactory(() => {
  return new Promise((resolve) => {
    fetch('/engineData.json')
      .then((data) => {
        resolve(new Engine(data));
      });
  });
})

.toAsyncFactory()

bind(Engine).toAsyncFactory(() => {
  return new Promise((resolve) => {
    fetch('/engineData.json')
      .then((data) => {
        resolve(new Engine(data));
      });
  });
})

More DI decorators

Injecting Promises

class Car {
  constructor(@InjectPromise(Engine) enginePromise) {
    enginePromise.then((engine) => {
      ...
    });
  }
}

Injecting Promises

class Car {
  constructor(@InjectPromise(Engine) enginePromise) {
    enginePromise.then((engine) => {
      ...
    });
  }
}

Injecting Promises

class Car {
  constructor(@InjectPromise(Engine) enginePromise) {
    enginePromise.then((engine) => {
      ...
    });
  }
}

Injecting Factory Functions

class Car {
  constructor(@InjectLazy(Engine) engineFn) {
    if (someCondition) {
      this.engine = engineFn();
    }
  }
}

Optional dependencies

class Car {
  constructor(@Optional @Inject(Engine) engine) {
    ...
  }
}

engine can be null

Problems solved in new DI:

  • Allows asynchronous and synchronous strategy - Solved!
  • No namespace conflicts - Solved!
  • Can be used standalone - Solved!
  • Dependencies are served as Singletons - ???

Transient Dependencies

Child Injectors

var inj = Injector.resolveAndCreate([
  Engine
]);

var childInj = Injector.resolveAndCreateChild([
  Engine
]);

childInj.get(Engine) !== inj.get(Engine)

Child Injectors

var inj = Injector.resolveAndCreate([
  Engine
]);

var childInj = Injector.resolveAndCreateChild([
  Engine
]);

childInj.get(Engine) !== inj.get(Engine)

Child Injectors

var inj = Injector.resolveAndCreate([
  Engine
]);

var childInj = Injector.resolveAndCreateChild([
  Engine
]);

childInj.get(Engine) !== inj.get(Engine)

How is it used in Angular 2 then?

@Component({
  selector: 'app'
})
@View({
  template: '

Hello {{name}}!

' }) class App { constructor() { this.name = 'World'; } } bootstrap(App);
@Component({
  selector: 'app'
})
@View({
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor() {
    this.name = 'World';
  }
}
          
bootstrap(App);
@Component({
  selector: 'app'
})
@View({
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor() {
    this.name = 'World';
  }
}
          
bootstrap(App);
@Component({
  selector: 'app'
})
@View({
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor() {
    this.name = 'World';
  }
}
          
bootstrap(App);
class NameService {
  constructor() {
    this.name = 'Pascal';
  }

  getName() {
    return this.name;
  }
}
bootstrap(App, [NameService]);
@Component({
  selector: 'app'
})
@View({
  template: '

Hello {{name}}!

' }) class App { constructor(@Inject(NameService) NameService) { this.name = NameService.getName(); } } bootstrap(App);
@Component({
  selector: 'app'
})
@View({
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor(@Inject(NameService) NameService) {
    this.name = NameService.getName();
  }
}
          
bootstrap(App);
@Component({
  selector: 'app'
})
@View({
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor(NameService: NameService) {
    this.name = NameService.getName();
  }
}
          
bootstrap(App);
@Component({
  selector: 'app',
  injectables: [NameService]
})
@View({
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor(NameService: NameService) {
    this.name = NameService.getName();
  }
}
          
bootstrap(App);

Thanks.

By @PascalPrecht
JSConf Budapest May, 2015