O zależnościach

Co sobie wstrzykują programiści?

Piotr Wandycz
Dla juniorów

Czym są zależności? Cóż, rzecz to równocześnie teoretyczna – jak i praktyczna. Wyobraź sobie, że jedziesz na wycieczkę do innego miasta i decydujesz się skorzystać z usług PKP. W tym momencie Twoja podróż zależy od Polskich Kolei Państwowych i istnieje pewna doza ryzyka, że coś pójdzie nie tak. A na stację dojedziesz tramwajem? Nie chciałbym Cię zmartwić, ale tu też ewentualny korek na drodze może sprawić, że nie zdążysz dojechać na dworzec – i tak jedna zależność wpłynie na drugą. Patrząc na to z innej strony – jeśli wszystko pójdzie dobrze, a jest spora szansa, że tak się stanie, to pojedziesz na tę wymarzoną wycieczkę. A gdyby nie te dwie zależności, to może w ogóle nie byłoby możliwości odbycia wyprawy? I tak w codziennej pracy – aplikacja musi wysłać maila po rejestracji użytkownika, a serwis pogodowy wyświetlić aktualną temperaturę. Zależymy od zewnętrznych dostawców, jak również i od naszego własnego kodu. Jeden serwis wyświetli dane z bazy na stronie A, a inny umożliwi użytkownikowi złożenie zamówienia.

Coupling is gonna kill you faster than anything else.

Ian Cooper, usłyszane na Wroc# (pisownia nieoryginalna, z pamięci)

Problemem nie jest sam fakt, że zależności istnieją – bo dzięki nim nasze aplikacje mogą realizować założenia naszych klientów. Problem pojawia się gdy tracimy kontrolę nad tym, co od czego jest zależne, oraz tworzą się miejsca silnie związane zależnościami (tight coupling), gdzie coraz trudniej jest nam wprowadzać zmiany w kodzie. Istnieje praktyka pisania luźno powiązanego kodu (loose coupling). Istnieją także różne metryki pozwalające nawet wyliczać poziom niestabilności aplikacji (instability), poprzez dzielenie zależności odprowadzających i dośrodkowych (zdecydowanie lepiej brzmi to po angielsku: efferent i afferent coupling). Dla małych projektów dobrym początkiem będzie obserwowanie projektu i wyłapanie często zmienianych miejsc w aplikacji oraz otoczenie tych kawałków kodu szczególną opieką.

Odwracanie zależności

Jednym z podstawowych wzorców ułatwiających radzenie sobie z zależnościami jest nauczenie się jak je odwracać. Odwrócenie zależności (Dependency Inversion) opisałbym jako rozszerzanie funkcjonalności klasy poprzez używanie zewnętrznych “serwisów”. Przykładowo: potrzebuję wybrać pytanie rekrutacyjne dla użytkownika, bazując na udzielonej przez niego wcześniej odpowiedzi. Odpowiedzialnością mojej klasy jest tu zwrócenie odpowiedniego modelu do odczytu (zgodnie z zasadą pojedynczej odpowiedzialności), a w jakiś sposób muszę dostarczyć logikę. Stosuję więc wstrzykiwanie zależności (Dependency Injection), czyli najpopularniejszy sposób na dostarczanie takiej zewnętrznej logiki. Doprecyzowując: wstrzykiwanie przez konstruktor, bo można też wstrzykiwać bezpośrednio do metody.

internal class QueryHandler : IRequestHandler<Query, ViewModel>
{
    private readonly IRepository _repository;
    private readonly IQuestionSelector _questionSelector;
    private readonly IAnswerOccurenceCalculator _answerOccurenceCalculator;

    public QueryHandler(IRepository repository, 
        IQuestionSelector questionSelector, 
        IAnswerOccurenceCalculator answerOccurenceCalculator)
    {
        _repository = repository;
        _questionSelector = questionSelector;
        _answerOccurenceCalculator = answerOccurenceCalculator;
    }

Dzisiaj może to wydawać się standardem i oczywistością – i bardzo dobrze jeśli tak jest. Wraz z wprowadzeniem .NET Core 1.0 w 2016 roku Microsoft zadbał o jedną bardzo ważną rzecz. Przy tworzeniu nowej aplikacji internetowej – do konstruktora kontrolera jest wstrzykiwany logger. Taki przykładowy projekt nie zawiera prawie w ogóle kodu, ale stosuje już tę zasadę (DI), co dobrze wskazuje dalszy kierunek. Automatyczne rozwiązywanie zależności jest luksusem, który doceni każdy, kto w przeszłości musiał stosować wielokrotnie zagnieżdżony konstruktor przy tworzeniu nowego obiektu.

Wracając do przykładu: mam tu trzy zależności. Repozytorium – aby pobrać wcześniejsze odpowiedzi użytkownika z bazy danych. Kalkulator odpowiedzi – do pogrupowania ich w wystąpienia, których do działania potrzebuje finalny serwis pobierający numer następnego pytania.

public async Task<ViewModel> Handle(Query query, CancellationToken cancellationToken)
{
    var model = new ViewModel();
    var questionIds = await _repository.GetQuestionIdsThatHasAnswerAsync();
    var answersDb = await _repository.GetAnswersAsync(query.UserId);
    var answers = MapAnswers(answersDb);
    var occurences = _answerOccurenceCalculator.GetData(answers);
    var nextQuestionId = _questionSelector.GetNextQuestionId(questionIds, occurences);
    var questionDb = await _repository.GetQuestionAsync(nextQuestionId);
    model.Question = MapQuestion(questionDb);
    model.Answer.QuestionId = model.Question.Id;
    return model;
}

Popatrzmy teraz na to samo, ale od drugiej strony. Do konstruktora wstrzykujemy interfejsy, określane często jako kontrakty. Osoba implementująca dany interfejs niejako podpisuje z nim umowę. “Ktoś będzie mógł użyć mojej implementacji, więc ja muszę spełnić wszystkie zadeklarowane w kontrakcie warunki (w tym przypadku metody), bo ktoś może być od nich zależny.”. Implementacje dla danego interfejsu możemy mieć różne, a nawet korzystać z nich na zmianę, jeśli używamy wzorca strategii. Ma tutaj jednak znaczenie sam fakt wprowadzenia dodatkowej warstwy abstrakcji do kodu – nasza klasa polega na zależnościach z zewnątrz, wie – co wywołuje oraz jaki rezultat zostanie zwrócony z metod, ale nie musi być świadoma – jak dostawca realizuje wewnątrz daną logikę.

O projektowaniu systemów można rozmawiać długimi godzinami i nie dojść do żadnych produktywnych wniosków: jeden będzie mówił, że ta dodatkowa abstrakcja jest potrzebna, a ktoś inny, że nie. Trzecia osoba powie, że klasa ma za dużo zależności, a czwarta nie zgodzi się z tym. Czasem warto być tymi wszystkimi czterema osobami naraz i spoglądając na rozwiązywany problem z różnych perspektyw – zadawać sobie trudne pytania, bo może oszczędzić to nam w przyszłości masę czasu.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *