@PascalPrecht #techtalks_NS

Dependency Injection

in Angular 2

By @PascalPrecht
Newstore Tech Talk, January 2016

Angular GDE
Working on ng2 docs
Trainer at @thoughtram
That's my soap bubble gun

Oh my gosh this talk on Angular 2's DI by @PascalPrecht blows my mind! #techtalks_NS

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);
}

How is this solved in Angular 1?

DI in Angular 1

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

function Car(Engine, Tires, Doors) {
  ...
}

app.service('Car', Car);

DI in Angular 1

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

function Car(Engine, Tires, Doors) {
  ...
}

app.service('Car', Car);

DI in Angular 1

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

function Car(Engine, Tires, Doors) {
  ...
}

app.service('Car', Car);

DI in Angular 1

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

function Car(Engine, Tires, Doors) {
  ...
}

app.service('Car', Car);

DI in Angular 1

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

function Car(Engine, Tires, Doors) {
  ...
}

app.service('Car', Car);

DI in Angular 1

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

function Car(Engine, Tires, Doors) {
  ...
}

app.service('Car', Car);

Function.toString()

"function Car(Engine, Tires, Doors) { ... }"

A RegEx extracts the parameters names.

Annotations

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

function Car(Engine, Tires, Doors) {
  ...
}

Car.$inject = ['Engine', 'Tires', 'Doors'];

app.service('Car', Car);

Annotations

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

function Car(Engine, Tires, Doors) {
  ...
}

Car.$inject = ['Engine', 'Tires', 'Doors'];

app.service('Car', Car);

DI in Angular 1

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

...

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

DI in Angular 1

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

...

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

DI in Angular 1

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

...

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

We still have some problems though:

  • Internal cache - Dependencies are served as Singletons
  • 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/core';

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

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

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


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

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

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/core';

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

Car.parameters = [
  [Engine],
  [Tires],
  [Doors]
];

What about testability?

import { Injector } from 'angular2/core';

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

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

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

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

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

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

var injector = Injector.resolveAndCreate([
  provide(Car, {useClass: Car}),
  provide(Engine, {useClass: MockEngine}),
  provide(Tires, {useClass: Tires}),
  provide(Doors, {useClass: Doors})
]);
          
var car = injector.get(Car);

More Provider Instructions

Let's say we have an API endpoint...

'http://api.myapp.com'

How can we make this an injectable?

Providing values

provide('APIEndpoint', {
  useValue: 'http://api.myapp.com'  
})
constructor(@Inject('APIEndpoint') api) {
  ...
}

Okay cool. But what if we have dependencies that aren't (and shouldn't be) available for DI?

class ExceptionHandler {
  constructor(rethrowException: boolean) {

  }
}

 

class ExceptionHandler {
  constructor(rethrowException: boolean) {

  }
}

We don't want to introduce a provider for boolean here.

Providing factories

provide(ExceptionHandler, {
  useFactory: () => {
    return new ExceptionHandler(false);
  }
})

Of course, a factory can have dependencies

Providing factories

provide(ExceptionHandler, {
  useFactory: (logger) => {
    return new ExceptionHandler(logger, false);
  },
  deps: [Logger]
})

Of course, a factory can have dependencies

Providing factories

provide(ExceptionHandler, {
  useFactory: (logger) => {
    return new ExceptionHandler(logger, false);
  },
  deps: [Logger]
})

Of course, a factory can have dependencies

We can have optional dependencies too!

Optional dependencies

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

engine can be null

Problems solved in new DI:

  • No namespace conflicts - Solved!
  • Can be used standalone - Solved!
  • Optional dependencies- Solved!
  • Dependencies are served as Singletons - ???

Dependencies are still singletons.

Transient Dependencies

Child Injectors

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

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

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

Child Injectors

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

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

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

Child Injectors

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

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

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

How is it used in Angular 2 then?

@Component({
  selector: 'app',
  template: '

Hello {{name}}!

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


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


@Component({
  selector: 'app',
  template: '

Hello {{name}}!

' }) class App { constructor() { this.name = 'World'; } }
@Component({
  selector: 'app',
  template: '

Hello {{name}}!

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

bootstrap(App);
class NameService {
  constructor() {
    this.name = 'Pascal';
  }

  getName() {
    return this.name;
  }
}
@Component({
  selector: 'app',
  template: '<h1>Hello {{name}}!</h1>'
})
class App {
  constructor() {
    this.name = 'World';
  }
}

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

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

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

bootstrap(App, [NameService]);

What if a child component needs a different NameService?

Every component in Angular 2 has its own Injector!

@Component({
  selector: 'child-component'





  template: '

Hello other {{name}}!

' }) class ChildComponent { constructor(nameService: NameService) { this.name = nameService.getName(); } }
@Component({
  selector: 'child-component',
  providers: [
    provide(NameService, {
      useClass: OtherNameService
    })
  ],
  template: '

Hello other {{name}}!

' }) class ChildComponent { constructor(nameService: NameService) { this.name = nameService.getName(); } }
@Component({
  selector: 'child-component',
  providers: [
    provide(NameService, {
      useClass: OtherNameService
    })
  ],
  template: '<h1>Hello other {{name}}!>/h1>'
})
class ChildComponent {
  constructor(nameService: NameService) {
    this.name = nameService.getName();
  }
}

There are more things to cover!

  • Dependency Qualifiers
  • View Bindings
  • Debugging
  • Plattform Injectors
  • ...

Thanks.

@PascalPrecht
Newstore Tech Talk, January 2016

Article: Dependency Injection in Angular 2