jakubszpil

Szczepienie kodu, czyli jak Typescript radzi sobie z Dependency Injection

Dependency Injection (DI) to wzorzec projektowy stosowany w celu zwiększenia modularności i testowalności kodu. Pozwala on na oddzielenie tworzenia obiektów od ich używania, co prowadzi do lepszej separacji odpowiedzialności oraz ułatwia zarządzanie zależnościami w projekcie. 🔗

W TypeScript DI można zaimplementować na różne sposoby, m.in. za pomocą funkcji wstrzykujących, kontenerów IoC oraz dekoratorów. Poniżej znajdziesz szczegółowe omówienie praktycznych sposobów implementacji DI w TypeScript wraz z przykładami i wskazówkami.


>>Spis treści

  1. Czym jest Dependency Injection?
  2. Zalety stosowania DI
  3. Podstawowy przykład Dependency Injection
  4. Wstrzykiwanie zależności przy pomocy funkcji
  5. Kontener IoC i automatyzacja DI
  6. Testowanie z wykorzystaniem DI
  7. Podsumowanie

>>Czym jest Dependency Injection?

Dependency Injection polega na przekazywaniu obiektów zależnych (tzw. zależności) do obiektu zamiast tworzenia ich bezpośrednio w jego wnętrzu. Dzięki temu możemy łatwo podmieniać zależności – np. na ich mocki podczas testowania – bez zmian w logice biznesowej.

DI zwiększa elastyczność kodu, ułatwia jego testowanie oraz pozwala na lepszą separację odpowiedzialności. Dzięki temu Twój kod staje się bardziej modularny, przejrzysty i łatwy w utrzymaniu. 💡


>>Zalety stosowania DI


>>Podstawowy przykład Dependency Injection

Rozważmy prosty scenariusz, w którym klasa UserService korzysta z UserRepository:

class UserRepository {
  getUser(userId: number): string {
    return `User ${userId}`;
  }
}

class UserService {
  private userRepository: UserRepository;

  constructor(userRepository: UserRepository) {
    this.userRepository = userRepository;
  }

  getUserName(userId: number): string {
    return this.userRepository.getUser(userId);
  }
}

const userRepository = new UserRepository();
const userService = new UserService(userRepository);

console.log(userService.getUserName(1)); // User 1

Wyjaśnienie:
W tym przykładzie UserRepository jest wstrzykiwany do UserService poprzez konstruktor. Dzięki temu możemy łatwo podmienić repozytorium np. w testach jednostkowych lub zamienić na inną implementację.


>>Wstrzykiwanie zależności przy pomocy funkcji

W większych aplikacjach zarządzanie zależnościami ręcznie może być uciążliwe. Możemy zastosować funkcję inject, która będzie przechowywać i dostarczać instancje klas (prosta wersja kontenera IoC):

const dependencies: Map<string, any> = new Map();

function inject<T>(dependency: new () => T): T {
  if (dependencies.has(dependency.name)) {
    return dependencies.get(dependency.name);
  }
  const dep = new dependency();
  dependencies.set(dependency.name, dep);
  return dep;
}

class UserRepository {
  getUser(userId: number): string {
    return `User ${userId}`;
  }
}

class UserService {
  protected userRepository = inject(UserRepository);

  getUserName(userId: number): string {
    return this.userRepository.getUser(userId);
  }
}

class ExtendedUserService extends UserService {}

const extendedUserService = inject(ExtendedUserService);

console.log(extendedUserService.getUserName(1)); // User 1

Wyjaśnienie:


>>Kontener IoC i automatyzacja DI

W rozbudowanych projektach warto rozważyć użycie gotowych rozwiązań, np. InversifyJS, które pozwalają korzystać z dekoratorów i automatycznie rozwiązywać zależności.

Przykład z użyciem InversifyJS:

import "reflect-metadata";
import { injectable, inject, Container } from "inversify";

@injectable()
class UserRepository {
  getUser(userId: number): string {
    return `User ${userId}`;
  }
}

@injectable()
class UserService {
  constructor(@inject(UserRepository) private userRepository: UserRepository) {}

  getUserName(userId: number): string {
    return this.userRepository.getUser(userId);
  }
}

const container = new Container();
container.bind(UserRepository).toSelf();
container.bind(UserService).toSelf();

const userService = container.get(UserService);
console.log(userService.getUserName(1)); // User 1

Zalety takiego podejścia:


>>Testowanie z wykorzystaniem DI

Dzięki zastosowaniu DI możemy łatwo podmieniać implementacje zależności, np. na mocki lub stuby podczas testów jednostkowych:

class MockUserRepository {
  getUser(userId: number): string {
    return "Mock User";
  }
}

const mockRepo = new MockUserRepository();
const userService = new UserService(mockRepo);

console.log(userService.getUserName(1)); // Mock User

Korzyści:


>>Podsumowanie

Dependency Injection w TypeScript to potężny sposób na zwiększenie elastyczności, testowalności i modularności kodu. Najprostsze podejście to ręczne wstrzykiwanie zależności przez konstruktor, jednak w miarę wzrostu projektu warto pomyśleć o własnym kontenerze IoC lub sięgnąć po gotowe biblioteki jak InversifyJS. DI pozwala na lepszą separację odpowiedzialności i sprawia, że kod jest łatwiejszy w utrzymaniu i testowaniu.


Dalsza lektura: 📚

Widzisz jakiś błąd, bądź literówkę? Chcesz coś poprawić?✏️ Przejdź do edycji tego pliku