Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Serwisy i Providers - specjaliści Imperium

Centurion z poprzedniej lekcji przyjmuje rozkazy i przekazuje je dalej. Ale komu? Gdyby sam liczył żołd, sprawdzał zapasy i pisał do skarbca, przestałby być centurionem, a stałby się jednoosobową kohortą - i tę samą pracę trzeba by powtórzyć w każdym innym oddziale.

Legion ma na to specjalistów: kowala, medyka, kwatermistrza. Każdy zna jedną rzecz i robi ją dla wszystkich. W NestJS takim specjalistą jest serwis.

Dekorator, który czyni specjalistę

Serwis to zwykła klasa z jednym dekoratorem:

1@Injectable()
2export class LegionService {
3  private legions: Legion[] = [];
4
5  findAll(): Legion[] {
6    return this.legions;
7  }
8
9  findById(id: number): Legion {
10    return this.legions.find((legion) => legion.id === id);
11  }
12
13  recruit(data: CreateLegionDto): Legion {
14    const legion = { id: Date.now(), ...data };
15    this.legions.push(legion);
16    return legion;
17  }
18
19  dismiss(id: number): void {
20    this.legions = this.legions.filter((legion) => legion.id !== id);
21  }
22}

@Injectable()
oznacza klasę jako provider, który może być wstrzykiwany. To jedyne, co robi - i warto to odgraniczyć od sąsiadów, bo dekoratory w NestJS wyglądają podobnie: kontroler oznacza
@Controller()
, moduł
@Module()
, a middleware to klasa implementująca
NestMiddleware
.
@Injectable()
nie czyni z klasy żadnego z nich - mówi tylko, że NestJS ma umieć ją komuś podać.

Cztery metody powyżej to typowy zestaw:

findAll()
zwraca wszystko,
findById(id)
jedną pozycję,
recruit(data)
tworzy nową,
dismiss(id)
usuwa. Nazwy opisują czynność w dziedzinie, nie czasownik HTTP - bo serwis nie wie, że istnieje jakiś HTTP.

Wstrzyknięcie do kontrolera

Gotowego specjalistę kontroler dostaje przez konstruktor:

1@Controller('legions')
2export class LegionController {
3  constructor(private readonly legionService: LegionService) {}
4
5  @Get()
6  findAll() {
7    return this.legionService.findAll();
8  }
9}

Kolejność zapisu jest stała:

constructor(
, potem
private readonly legionService: 
, dalej
LegionService
, na końcu
) {}
.

Ten skrót zasługuje na wyjaśnienie, bo wygląda dziwnie. Słowo

private
przed parametrem konstruktora to skrót TypeScriptu: deklaruje pole klasy i przypisuje mu wartość w jednym miejscu. Bez niego musiałbyś napisać
private legionService: LegionService
osobno i
this.legionService = legionService
w ciele konstruktora.
readonly
dokłada gwarancję, że nikt tego pola później nie podmieni.

Sam nie tworzysz instancji przez

new
- robi to NestJS. Patrzy na typ parametru, znajduje pasujący provider i podaje go gotowego. To jest wstrzykiwanie zależności: klasa mówi, czego potrzebuje, a nie skąd to wziąć.

Serwis w serwisie

Specjaliści korzystają nawzajem ze swoich usług tak samo. Droga ma cztery kroki i warto ją znać w kolejności:

  1. Dodanie
    @Injectable()
    do nowego serwisu.
  2. Import modułu z eksportowanym serwisem - jeśli pochodzi z innego modułu.
  3. Wstrzyknięcie serwisu przez konstruktor.
  4. Wywołanie metody wstrzykniętego serwisu.
1@Injectable()
2export class PayrollService {
3  constructor(private readonly legionService: LegionService) {}
4
5  calculateTotalPay(): number {
6    const legions = this.legionService.findAll();
7
8    return legions.reduce((sum, legion) => sum + legion.pay, 0);
9  }
10}

Krok drugi jest tym, o którym najczęściej się zapomina. Provider zadeklarowany w module nie jest automatycznie widoczny gdzie indziej - musi być wypisany w

exports
swojego modułu, a moduł, który go potrzebuje, musi go zaimportować. Pominięcie tego daje przy starcie błąd o nierozwiązanej zależności.

Oddzielenie dostępu do danych

Nasz

LegionService
trzyma dane w tablicy. Gdy przyjdzie prawdziwa baza, każda metoda będzie musiała się zmienić - a razem z nią wszystko, co jej używa.

Rozwiązaniem jest Repository Pattern - wzorzec, który oddziela logikę dostępu do danych od logiki biznesowej. Serwis mówi wtedy, co chce zrobić, a repozytorium wie, jak dane pobrać. Zmiana tablicy na bazę dotyka wyłącznie repozytorium.

Nie myl go z innymi wzorcami o podobnie brzmiących nazwach: Singleton pilnuje, by istniała jedna instancja, Observer rozsyła powiadomienia o zdarzeniach, Proxy podstawia zastępczy obiekt w miejsce prawdziwego. Tylko Repository dotyczy rozdzielenia danych od logiki.

Provider z fabryki

Zwykle wystarczy podać klasę i NestJS sam ją utworzy. Czasem jednak instancja zależy od czegoś, co znasz dopiero przy starcie - wtedy podajesz przepis zamiast klasy:

1@Module({
2  providers: [
3    {
4      provide: 'STORAGE_SERVICE',
5      useFactory: (config: ConfigService) => {
6        return config.get('STORAGE') === 'cloud'
7          ? new CloudStorageService()
8          : new LocalStorageService();
9      },
10      inject: [ConfigService],
11    },
12  ],
13})
14export class StorageModule {}

Trzy pola opisują ten przepis.

provide
to nazwa, pod którą provider będzie dostępny.
useFactory
to funkcja tworząca instancję - tu wybiera między dwiema implementacjami zależnie od konfiguracji.
inject
wylicza, czego potrzebuje sama fabryka; NestJS poda jej to jako argumenty, w tej samej kolejności.

Zauważ, po co to całe zamieszanie: reszta aplikacji prosi o

'STORAGE_SERVICE'
i nie wie, którą implementację dostała. Podmiana chmury na dysk lokalny to zmiana w jednym miejscu, @name - a nie w każdym serwisie, który zapisuje pliki.

Podsumowanie

Specjaliści na miejscu, każdy zna swoją rzecz:

  • serwis trzyma logikę, kontroler tylko kieruje żądania,
  • @Injectable()
    oznacza klasę jako provider, który może być wstrzykiwany
    - nie czyni jej kontrolerem, modułem ani middleware,
  • nazwy metod opisują czynność w dziedzinie (
    findAll
    ,
    findById
    ,
    recruit
    ,
    dismiss
    ), nie czasowniki HTTP,
  • wstrzyknięcie w kolejności:
    constructor(
    ,
    private readonly nazwa: 
    ,
    TypSerwisu
    ,
    ) {}
    ,
  • private
    przed parametrem konstruktora deklaruje pole i przypisuje wartość w jednym miejscu,
  • instancji nie tworzysz przez
    new
    - NestJS dobiera provider po typie,
  • serwis w serwisie:
    @Injectable()
    → import modułu z
    exports
    → wstrzyknięcie przez konstruktor → wywołanie metody
    ,
  • provider nie jest widoczny poza swoim modułem, dopóki nie trafi do
    exports
    ,
  • Repository Pattern oddziela logikę dostępu do danych od logiki biznesowej - Singleton, Observer i Proxy rozwiązują zupełnie inne problemy,
  • provider z fabryki opisują
    provide
    (nazwa),
    useFactory
    (funkcja tworząca) i
    inject
    (zależności fabryki),
  • dzięki fabryce reszta aplikacji nie wie, którą implementację dostała.

W następnej lekcji zbudujesz z tych elementów pierwsze REST API - komplet operacji CRUD na jednym zasobie. A na razie zapamiętaj: kontroler wie kto ma coś zrobić, serwis wie jak - i tylko dzięki temu tę samą wiedzę wywołasz z dowolnego miejsca aplikacji.

Przejdź do CodeWorlds