Inversion of Control - okładka

Inversion of Control z użyciem Dependency Injection

Opublikowano Kategorie Czysty kodCzas czytania 7min

Inversion of Control to szalenie przydatny wzorzec postępowania w programowaniu obiektowym. W tym artykule dowiesz się, na czym polega Inversion of Control (lub też swojskie — odwrócenie sterowania), a także poznasz jedną z najczęściej spotykanych form zastosowania odwrócenia sterowania, czyli Dependency Injection (wstrzyknięcie zależności). Wszystkie przykłady kodu przygotowałem w TypeScript.

Inversion of Control

Odpowiadając na pytanie, czym jest Inversion of Control, często odpowiedź zawiera nawiązanie do Hollywood Principle.

Don’t call us, we’ll call you.

O ile samo porównanie jest jak najbardziej trafne, tak bez odpowiedniego wytłumaczenia i kontekstu jest enigmatyczne i nie odpowiada w pełni na postawione pytanie. Odwrócenie sterowania jest odpowiedzią na problem zależności pomiędzy klasami w kodzie.

Przede wszystkim należy sobie uświadomić, dlaczego posiadanie zależności w kodzie jest problemem. Po pierwsze, posiadanie zależności w kodzie prowadzi do powstawania wysokiego couplingu między klasami, co w konsekwencji może powodować sytuacje, gdzie zmiana w kodzie jednej klasy wymusza zmianę w kodzie innej klasy. Co gorsza, ta zmiana może być konieczna do zaaplikowania w wielu miejscach.

Drugim problemem wynikającym z powstawania zależności w kodzie jest problem z napisaniem dobrych testów jednostkowych. Powiązane klasy mogą okazać się problematyczne, a czasami wręcz niemożliwe do przetestowania z użyciem testów jednostkowych. Poniżej znajdziesz fragment kodu posiadający omawiane problemy.


import axios from  'axios';

class WeatherStatus {
    public async getTemperature( city: string ): Promise {
        const httpClient: IHttpClient = axios.create( {
            baseURL: 'https://some.api.com'
        } );

        const temperature: number = await httpClient.get( `api/v1/temperature?city=${ city }` );

        return `Temperature in ${ city }: ${ temperature }`;
    }
}

Przedstawiona klasa posiada jedną publiczną metodę getTemperature, która dla podanego miasta zwraca temperaturę. Wydawać by się mogło, że powyższy kod spełnia swoje zadanie i nie ma powodów do niezadowolenia. Otóż moim zdaniem są.

Przede wszystkim tego kodu nie da się dobrze przetestować z użyciem testów jednostkowych. W obecnym stanie nie istnieje możliwość stworzenia mocka klienta HTTP oraz odpowiedzi z metody get. Jedno z założeń testów jednostkowych mówi, że wszystkie zewnętrzne komponenty (czyli w omawianym przypadku klient HTTP) powinny zostać zmockowane (choć też nie zawsze). Więcej o testach, w tym testach jednostkowych możesz dowiedzieć się z artykułu Podstawy testów automatycznych oprogramowania. Wywołanie metody w teście spowoduje faktyczne wykonanie zapytania do API, także z pewnością nie będzie to test jednostkowy. Będzie to test integracyjny.

Drugim problemem przedstawionego kodu jest jawna inicjalizacja instancji klienta HTTP wraz z bezpośrednim podaniem konfiguracji. Będzie to problematyczne zarówno podczas pisania testów, jak i podczas potencjalnych refactorów w przyszłości. Co więcej, występuje tu jawne konfigurowanie niskopoziomego komponentu w klasie logiki biznesowej. Powoduje to mieszanie się warstw aplikacji i ogólny regres jakości kodu.

Kolejny problem powstanie w momencie chęci zmiany Axiosa na jakiekolwiek inne rozwiązanie. Załóżmy, że klas takich jak WeatherStatus mamy w systemie kilka/kilkanaście/kilkadziesiąt. W takim przypadku, w każdym miejscu należy dotknąć kodu biznesowego z powodu podmiany niskopoziomowego komponentu. Jest to złe nie tylko z powodu konieczności wykonania żmudnej pracy, ale też z powodu potencjalnego ryzyka uszkodzenia kodu biznesowego. Przypominam, że powyższy kod jest problematyczny do przetestowania z użyciem testów jednostkowych, co tylko podwyższa potencjalne ryzyko.

Rozwiązanie problemu

Rozwiązaniem problemu jest dokonanie Inversion of Control poprzez pozbycie się zależności pomiędzy klientem HTTP a klasą WeatherStatus. Przede wszystkim klient HTTP powinien być konfigurowany na zewnątrz, czyli spoza klasy WeatherStatus. Klasa WeatherStatus powinna dostać gotowy komponent do komunikacji z API — skonfigurowany, działający i gotowy do działania. Pozbycie się zależności między klientem HTTP a klasą WeatherStatus pozwoli też na przetestowanie zarówno klasy biznesowej, jak i klienta HTTP z użyciem testów jednostkowych. Ponadto, wyeliminowana zostanie konieczność modyfikacji kodu logiki biznesowej w przypadku podmiany niskopoziomowego komponentu.

Jeśli omówiony problem i rozwiązanie wydają Ci się znajome i kojarzą ci się z piątą zasadą SOLID, czyli Dependency Inversion Principle, to jest to dobre skojarzenie. Jeśli natomiast nie kojarzysz zasad SOLID, to serdecznie zachęcam do nadrobienia zaległości i przeczytanie artykułu SOLID, KISS i DRY.

Dependency Injection

Dependency Injection, czyli wstrzyknięcie zależności jest jedną z form implementacji Inversion of Control. Warto w tym miejscu podkreślić, że wykorzystanie Dependency Injection jest równoznaczne z wykorzystaniem Inversion of Control, lecz wykorzystanie Inversion of Control nie oznacza wykorzystania Dependency Injection.

Cała magia w przedstawionym przeze mnie przykładzie polega na wstrzyknięciu do klasy WeatherStatus klienta HTTP. Jednym z wariantów Dependency Injection jest wstrzyknięcie z wykorzystaniem konstruktora. W omawianym przypadku wydaje się on mieć najwięcej sensu. Rezultat przeprowadzonego refactoru i wykorzystanie Dependency Injection znajdziesz w przykładzie poniżej.


interface IHttpClient {
    get( url: string ): Promise;
}

class WeatherStatus {
    public constructor( private readonly _httpClient: IHttpClient ) {}

    public async getTemperature( city: string ): Promise {

        const temperature: number = await this._httpClient.get( `api/v1/temperature?city=${ city }` );

        return `Temperature in ${ city }: ${ temperature }`;
    }
}

Tak banalny zabieg, jak odseparowanie klienta HTTP od klasy z logiką biznesową poprzez wstrzyknięcie sprawił, że:

  • zależność jest konfigurowana z zewnątrz. Klasa biznesowa nie wie nic o konfiguracji klasy niskopoziomowej;
  • możliwe staje się przetestowanie klasy WeatherStatus z użyciem testów jednostkowych. Zamiast klienta HTTP można w testach wstrzyknąć mocka, który zamiast wysyłać zapytanie, będzie zwracał uprzednio przygotowaną odpowiedź;
  • zamiana jednego klienta na innego nie będzie wymagało dotknięcia kodu logiki biznesowej. Jedynym wymaganiem dla nowego klienta HTTP w przypadku TypeScripta jest spełnianie warunków kontraktu zdefiniowanego w interfejsie IHttpClient.

Kiedy nie stosować Inversion of Control?

Na pierwszy rzut oka ciężko jest znaleźć jakieś istotne mankamenty odwrócenia sterowania. O ile kod z odwróconym sterowaniem jest prostszy w testowaniu, zmianie i rozbudowie, tak dla niektórych może on być nieco bardziej skomplikowany. Szczególnie widoczne może to być w przypadku dużych klas. Przedstawiony we wpisie przykład nie oddaje wystarczająco istoty problemu. Ponadto, nie w każdej aplikacji wystąpią testy i przyszła rozbudowa. W przypadku mikro projektów czy prostych skryptów/procedur taka inżynieria może okazać się armatą na muchę. W przypadku gdy nie potrzebujemy testów oraz jesteśmy pewni, że pisany kod w najbliższej przyszłości nie będzie zmieniany warto rozważyć podążanie za zasadą YAGNI (You Ain’t Gonna Need It). Oczywiście każdy przypadek należy rozważyć indywidualnie. Nie istnieje bowiem żadna reguła pozwalająca określić, kiedy Inversion of Control jest konieczne, a kiedy nie. Niemniej jednak, w pewnych sytuacjach lepsze jest wrogiem dobrego 🙂

Podsumowanie

Pisząc ten artykuł, bardzo mi zależało, by wytłumaczyć omawiane zagadnienia jak najprościej. Zdaję sobie sprawę, że przeczytanie artykułu często pozwala zrozumieć temat, ale gdy przychodzi czas na praktyczne wykorzystanie wiedzy, to pojawia się problem. Dlatego też zachęcam do wypróbowania Dependency Injection w jednej z napisanych przez Ciebie aplikacji. Bardzo zachęcam też do zapoznania się ze źródłami i materiałami dodatkowymi. Zestawienie informacji z kilku źródeł pozwoli Ci bardziej zrozumieć temat, poznać inne (być może bardziej zrozumiałe dla Ciebie) przykłady i punkty widzenia.

Zachęcam też do pozostawienia komentarza oraz udostępnienia wpisu, jeśli dowiedziałeś/aś się dzięki niemu czegoś przydatnego 🙂

Źródła i materiał dodatkowe:

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

0 komentarzy
Inline Feedbacks
View all comments