Zaczynamy
Upewnij się, że masz zainstalowaną najnowszą wersję TypeScript (aktualnie 2.7.0). Do szybkiego testowania kodu przyda się też ts-node, więc warto go doinstalować.
Zaczynam od skonfigurowania projektu w TypeScripcie. To nigdy nie było prostsze niż teraz:
npm init
tsc init --strictNastępnie tworzę dwa pliki: index.ts i injector.ts. W tym pierwszy zawrę kod mojej „aplikacji”, a w tym drugim zaimplementuję Dependency Injection.
Plan
Zasada działania dependency injection nie jest trudna i opisałem ją niegdyś w artykule:

Wzorce Projektowe: Dependency Injection
Wiele razy wspominałem o wstrzykiwaniu zależności, nigdy jednak nie wytłumaczyłem tego konceptu do końca. Na czym polega wzorzec Dependency Injection i jakie problemy rozwiązuje? W tym artykule chciałbym odpowiedzieć na te pytania oraz omówić teorię stojącą za wstrzykiwaniem zależności.
Zanim jednak zacznę cokolwiek programować, warto byłoby mieć jakiś plan ;) Oto moje potrzeby i wymagania:
- możliwość rejestrowania zależności
- możliwość instancjonowania klas razem z automatycznie wstrzykniętymi zależnościami
Przykładowy kod:
class Foobar {
  constructor(public foo: Foo, public bar: Bar) {}
}
const foobar = Injector.resolve(Foobar);
foobar.foo; // jest tutaj wstrzyknięty!
foobar.bar; // też jest tutaj!Nie brzmi strasznie, prawda? Aby zrealizować te dwa podpunkty muszę jednak skorzystać z techniki zwanej refleksją.
Refleksja
Pragnę, aby w moim Dependency Injection  zależności były wstrzykiwane automatycznie na podstawie typu argumentów przekazanych do konstruktora. Z pomocą przychodzi właśnie refleksja oraz paczka reflect-metadata:
npm install reflect-metadata --saveSłuży ona do wydobywania pewnych metadanych z obiektów. Te metadane są dodawane do, między innymi, klas, na których użyto jakiegoś dekoratora. Nie wnikam na razie w powody takiego stanu rzeczy, wiem tylko jedno: Na każdej klasie, którą chcę wstrzykiwać, muszę użyć dekoratora.
Dekorator
Dekorator to po prostu funkcja, która przyjmuje jako argument np. klasę i może ją zmodyfikować. Nic szczególnego, prawda? Brzmi prosto. Najprostszy dekorator wygląda tak:
const Injectable = Target => {}a wykorzystać go można w ten sposób:
@Injectable
class X {}Możliwe jest też stworzenie fabryki dekoratorów, czyli funkcji, która zwraca dekorator. Jest to rozwiązanie znacznie bardziej popularne, bo daje dużo szersze możliwości:
const Injectable = () => {
  return Target => {};
};
@Injectable()
class X {}Z tej formy będę też korzystał dalej.
Dekoratory a typy?
Domyślnie TypeScript dostarcza jeden typ ClassDecorator — ale jest on dość ograniczony bo przede wszystkim nie jest generyczny. Dlatego napiszę kilka własnych typów do tego. Na początek potrzebuję typ dla „czegoś co mogę wywołać new” — czyli dla klasy albo konstruktora. Zapisuję to w ten sposób:
interface Constructor<T> {
  new (...args: any[]): T;
}przyda się też nieco bardziej rozbudowany typ dla dekoratora klasy:
type ClassDecorator<T extends Function> = (Target: Constructor<T>) => T | void;Czyli jest to typ generyczny, który jako argument typu T przyjmuje coś co rozszerza funkcję (czyli funkcję lub klasę). ClassDecorator<T> opisuje funkcję, która jako argument przyjmuje Constructor<T> i zwraca T.
Ostatecznie mój dekorator Injectable przyjmuje taką postać:
export const Injectable = (): ClassDecorator<any> => {
  return target => {};
};Injector
Mam już dekorator, a więc mam też metadane. Teraz mogę napisać serwis — Injector — który będzie odpowiedzialny za tworzenie instancji klas wraz ze wstrzykniętymi zależnościami. Injector będzie singletonem z jedną tylko metodą — resolve<T>(Target: Constructor<T>): T.
Pobieranie typów
Na początek pobieram typy argumentów przekazanych do konstruktora Target:
Reflect.getMetadata('design:paramtypes', Target)Ta metoda zwraca tablicę konstruktorów. Przykładowo, załóżmy że mam klasy Foo oraz Bar, a klasa X wymaga ich w konstruktorze:
@Injectable()
class X {
  constructor(foo: Foo, bar: Bar) {}
}
Reflect.getMetadata('design:paramtypes', X); // [Foo, Bar]Tworzenie zależności
Teraz dla każdego argumentu muszę wywołać Injector.resolve(…) — na wypadek gdyby np. Foo również miało w konstruktorze jakieś zależności. Następnie sprawdzone zostaną zależności Foo, a potem zależności zależności Foo, a potem zależności zależności zależności Foo… i tak dalej. Gdy już dojdę do klasy, która nie ma żadnych zależności — muszę po prostu stworzyć jej instancję przez new. Brzmi skomplikowanie? Nie, to tylko kilka linii kodu:
export const Injector = new class {
  resolve<T>(Target: Constructor<T>): T {
    const requiredParams = Reflect.getMetadata('design:paramtypes', Target) || [];
    const resolvedParams = requiredParams.map((param: any) => Injector.resolve(param));
    const instance = new Target(...resolvedParams);
    return instance;
  }
}();Efekt
Testy prostego DI:
import { Injector, Injectable, Constructor } from './src/injector';
@Injectable()
class NoDeps {
  doSth() {
    console.log(`I'm NoDeps!`);
  }
}
@Injectable()
class OneDep {
  constructor(public noDeps: NoDeps) {}
  doSth() {
    console.log(`I'm OneDep!`);
  }
}
@Injectable()
class MoarDeps {
  constructor(public noDeps: NoDeps, public oneDep: OneDep) {}
  doSth() {
    console.log(`I'm MoarDeps!`);
  }
}
const moarDeps = Injector.resolve(MoarDeps);
moarDeps.doSth();
moarDeps.noDeps.doSth();
moarDeps.oneDep.doSth();
moarDeps.oneDep.noDeps.doSth();Oraz efekt działania:
I'm MoarDeps!
I'm NoDeps!
I'm OneDep!
I'm NoDeps!Podsumowanie
Jak widzisz, wszystkie zależności zostały automatycznie wstrzyknięte na podstawie typów klas przekazanych do konstruktora! Great success! 😎
Cały kod znajdziesz tutaj: github.com/mmiszy/typeofweb-dependency-injection-typescript
Nie obsługuję jednak kilku rzeczy:
- circular dependencies (gdy Foo zależy od Bar, a Bar od Foo)
- innych typów niż własne klasy
- nie cache'uję stworzonych instancji klas, więc przy każdym wstrzyknięciu tworzone są nowe (to może być problem!)
- nie daję możliwości łatwego mockowania klas w injectorze (często ważny element DI)
W kolejnym wpisie postaram się dopisać coś z tej listy ;)
Podobało się?
Napisz w komentarzu! Jeśli uważasz, że to kompletnie bzdury — to również napisz :) Albo może zapisz się na szkolenie z TypeScript.
