#3a. Nie Zapominaj O Angularze /Dependency Injection/

in #strimi7 years ago

Parę słów wstępu

Witam użytkowników Strimi! Jest to moja pierwsza treść dodana przez ten interfejs sieci Steem. W samym blockchainie jestem aktywny już od maja 2017, moje główne konto to bowiem @jakipatryk. Nie przedłużając - zapraszam do trzeciej już części kursu Angulara, który przygotowuję od jakiegoś czasu - tym razem będzie o czymś, bez czego trudno sobie wyobrazić pracę z tym frameworkiem. Jest jeszcze coś, bez czego nie wyobrażam sobie Ciebie czytającego ten post - znajomość dwóch pierwszych części kursu. Jest ona konieczna do zrozumienia znacznej części tego wpisu.

Poprzednie części:

  1. Ustawiamy projekt
  2. Na początku był chaos

Github

Cały kod znajdziesz na Githubie, a branch na podstawie którego powstała ta część jak i powstaną następne dwie (3b i 3c) znajdziesz tu.


Dependency Injection

To, o czym będę dziś pisał świetnie podsumował swego czasu James Shore (źródło):

"Dependency Injection" is a 25-dollar term for a 5-cent concept.



Istnieje duże prawdopodobieństwo, że o czymś takim jak Dependency Injection (dalej nazywane w skrócie DI) już wcześniej słyszałeś. Są szanse, że na pierwszy rzut oka ta koncepcja wydawała Ci się trudna do zrozumienia. "Nic bardziej mylnego", DI jest proste, a ja spróbuję to udowodnić.

Dlaczego?

Najprościej jest zacząć od powodów dla których DI jest tak powszechnie używany. Spójrz na taki kod:

class Rocket {
    private engine: Engine;
    private propellantTank: PropellantTank;
    constructor() {
        this.engine = new Engine();
        this.propellantTank= new PropellantTank();
    }
    startEngine() {
        this.engine.start();
        console.log('Engine has just started!');
    }
}

Cóż można o nim powiedzieć? Deklarujemy klasę, która w swoim konstruktorze tworzy wszystkie potrzebne jej zależności (dependencies). I to jest problematyczne - klasa nie powinna wiedzieć jak stworzyć te zależności, dlaczego?

Po pierwsze sprawia to, że klasa jest trudna do przetestowania. Nie jest łatwo w tak napisanym kodzie zamockować (stworzyć atrapę obiektu na potrzeby testów). A przecież każdy developer chce pisać kod testowalny, jest to dobra praktyka.

Możesz sobie również wyobrazić co się stanie gdy będzie Ci potrzebny obiekt stworzony na podstawie tej klasy, lecz zamiast Engine będziesz potrzebować jego ulepszonej wersji SuperEngine. Tak długo jak zależności są tworzone wewnątrz klasy utrudniasz sobie pracę. Jak widzisz, powyższa klasa jest nie tylko nietestowalna, ale również nieelastyczna.

Co więc możemy uczynić? Okazuje się, że istnieje proste rozwiązanie - klasa powinna poprosić o zależności z zewnątrz zamiast tworzyć je wewnątrz swojej struktury.

Z pomocą w takich przypadkach przychodzi nam DI jako wzorzec projektowy.

Jak?

Jak najprościej:

class Rocket {
    private engine: Engine;
    private propellantTank: PropellantTank;
    constructor(engine, propellantTank) {
        this.engine = engine;
        this.propellantTank = propellantTank;
    }
    startEngine() {
        this.engine.start();
        console.log('Engine has just started!');
    }
}

tak napisana klasa umożliwia nam mockowanie zależności na potrzeby testów:

const testRocket = new Rocket(new MockEngine(), new MockPropellantTank());

oraz utworzenie na jej podstawie obiektu z dowolną wersją silnika:

const falcon9 = new Rocket(new SuperEngine(), new PropellantTank());

Jak widzisz, powiedzieliśmy konstruktorowi, że jedyne czego potrzebuje to dwie zależności, które dostanie z zewnątrz. Nie musi więc wiedzieć, jak je utworzyć, a jedynie to, że je dostanie. Odpowiedzialność za utworzenie ich przenieśliśmy do świata zewnętrznego.

Super! Wyeliminowaliśmy nasze problemy! I to już jest Dependency Injection jako wzorzec projektowy. Nie było to takie straszne, prawda?

DI jako framework

Jesteśmy szczęśliwi, ponieważ nasza klasa Rocket jest taka super. Ale czy pomyśleliśmy o jej konsumentach? Co jeśli Rocket potrzebowałaby nie 2, a na przykład 15 zależności? Utrudniałoby to pracę innym developerom, którzy chcieliby utworzyć instancję naszej klasy. Rozwiązniem tego problemu jakiemu się przyjrzymy będzie DI jako framework.

Okazuje się, że rozwiąże on wszystkie nasze problemy. Klasa nie będzie wiedziała jak utworzyć potrzebne jej zależności, a konsument nie będzie wiedział jak utworzyć instancję klasy. Magia, prawda? Wyobraź sobie, że następujący kod jest prawdziwy:

const injector = new Injector();
const saturnV = injector.get(Rocket);
saturnV.startEngine();

Tworzymy Injector, który za pomocą metody get przekazuje nam potrzebną nam instancję klasy. Byłoby świetnie, gdybyśmy mogli tak zrobić! Z pomocą przychodzi nam właśnie framework DI.

Serwisy w Angularze

Już wiesz, czym jest DI, zarówno jako wzorzec projektowy jak i jako framework. Wypada więc przyjrzeć się teraz, jak używać zaimplementowanego w Angularze DI w naszych aplikacjach.

Niech fragment serwisu TaskService będzie naszym przykładem:

@Injectable()
export class TaskService {
  constructor(private afs: AngularFirestore, private afAuth: AngularFireAuth) {}
  getTask(taskId: string, userId: string) {
    return this.afs.doc<Task>(`users/${userId}/tasks/${taskId}`);
  }
}

Jak widzisz, TaskService potrzebuje 2 zależności (AngularFirestore i AngularFireAuth), a odpowiedzialność za ich dostarczenie przenieśliśmy poza klasę.

To, że kod działa zobaczyliście już w poprzedniej części, skąd więc Angular wie, że musi wstrzyknąć (incject) jakieś zewnętrzne zależności do konstruktora TaskService?

Odpowiedź jest prosta: nie wie dopóki mu nie powiemy. Jak więc udało nam się go do tego zmusić?

Okazuje się, że Angular wstrzykuje zależności do konstruktora klasy tylko wtedy, gdy ta klasa posiada dekorator, np. @Injectable() lub @Component(). Przykład z tym pierwszym dekoratorem masz wyżej, więc spójrz na taką klasę, która posiada dekorator @Component i również potrzebuje wstrzyknięcia zależności (i oczywiście je dostaje):

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  constructor(private taskService: TaskService, private authService: AuthService) {}
}

Wiemy już, że Angular w jakiś magiczny sposób za pomocą swojego injectora wstrzykuje instancje klas, o które prosi dana klasa w konstruktorze. No właśnie, instancje klas - ale jakie instancje? Aby odpowiedzieć na to pytanie musimy przyjrzeć się providerom.

Providerzy

Jestem pewien, że już o nich słyszałeś. Nie, @jakipatryk, nie słyszałem. Jeśli jeszcze tego nie zrobiłeś, to zapraszam do części 2 kursu, gdzie powiedziałem, że @NgModule() pobiera pewne opcje konfiguracyjne. Jedną z nich była kolekcja providers, którą opisałem jako idealne miejsce na umieszczenie naszych serwisów.

Nie kłamałem. Jest to rzeczywiście świetne miejsce do zarejestrowania serwisów. Ale nie jest jedynym możliwym miejscem. Okazuje się, że kolekcję providers w opcjach konfiguracyjnych posiadają nie tylko dekoratory modułu, ale także np. komponentu:

@Component({
  selector: 'app-root',
  providers: [TaskService, AuthService],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  constructor(private taskService: TaskService, private authService: AuthService) {}
}

Jaka jest różnica? Miejsce rejestracji serwisów jest kluczowe. Decyduje o zasięgu instancji serwisu jak i o jego czasie istnienia. Jeśli zadeklarujemy serwis w dekoratorze komponentu, będzie on istniał tylko w trakcie przez czas życia komponentu, a taki zarejestrowany w module będzie żył przez cały okres działania aplikacji.

Ale czym tak właściwie są providerzy? Jedna z definicji brzmi: provider opisuje jak Injector powinien być skonfigurowany. Aby lepiej to zrozumieć, musisz zdać sobie sprawę, że providerzy, których widziałeś wcześniej, są tak naprawdę skróconą wersją:

providers: [TaskService, AuthService]

jest skróconą wersją tego:

providers: [{ provide: TaskService, useClass: TaskService }, { provide: AuthService, useClass: AuthService }]

Teraz powinno być bardziej zrozumiałe, że injector zawsze, gdy potrzebuje TaskService lub AuthService, ma miejsce, aby sprawdzić, czego tak naprawdę potrzebuje.

Dodatkowo, serwisy są singletonami, ale tylko w zasięgu injectora. Z tego wynika, że zasięg instancji serwisów zależy od miejsca, gdzie zostały zarejestrowane.

Istnieje jeden root injector utworzony podczas startowania aplikacji, w naszym przypadku używa go AppModule. Jednak każda klasa posiadająca dekorator z zadeklarowaną kolekcją providerów podczas tworzenia jej instancji dostaje swój injector.

Podsumowanie

W tym wpisie opisałem Dependency Injection jako wzorzec projektowy, a następnie jako framework. Pokazałem również w skrócie jak działa framework DI w Angularze. Jak zwykle więcej o tym można przeczytać w artykułach z sekcji Linki, które warto sprawdzić.


Linki, które warto sprawdzić:

Wzorzec DI:

DI w Angularze:


Lubisz naukę i technologię? Sprawdź #steemstem oraz @steemstem!


Sort:  

Chyba strimi lekko zespuł tagi: plangular, pljavascript. O ile to nie zamierzone oczywiście :)

@dimmu czek dis ;)

Dobry art, mimo, że bardziej wdrażam się w reacta. Od angulara odrzuciła mnie rozbieżność wersji itp.