SOLID z easyGALib: Zasada odwracania zależności

Nie wiem czy pamiętasz, ale kiedyś tłumacząc dlaczego wybrałem taki projekt, powiedziałem że jednym z argumentów jest to, że będę w stanie poruszyć szereg tematów, z którymi początkujący i średnio-zaawansowany programista powinien być co najmniej zaznajomiony. Okazja nadarzyła się szybciej niż się spodziewałem, bo już na etapie projektowania, gdzie pojawia się zasada odwracania zależności.

W trakcie pracy nad architekturą easyGALib zastanawiałem się jak najlepiej będzie użyć biblioteki z punktu widzenia użytkownika. Pierwsza myśl to użycie dziedziczenia po klasie bazowej charakterystycznej dla wybranego rodzaju chromosomu. Co jednak jeżeli ktoś nie będzie chciał z jakiegoś powodu dziedziczyć po naszej klasie? Chyba to jednak słaby pomysł. Trzeba więc iść drugą, znacznie bezpieczniejszą drogą, a mianowicie użyć interfejsu, który będzie dla nas gwarancją, że to co nam dostarczył użytkownik do biblioteki ma odpowiednie metody i właściwości. Moja propozycja na początek to następujący interfejs:

public interface GeneticAlgorithm
{
    double GetFitness(Chromosome chromosome);
    IGAParameters Parameters { get; set; }
}

Na obecnym etapie zakładam, że użytkownik wrzuci do naszej biblioteki cokolwiek, co w wybrany przez niego sposób policzy wartość dopasowania konkretnego chromosomu. Dodatkowo też dorzuci obiekt zawierający parametry działania programu. W poście o algorytmach genetycznych pisałem, że stosujemy je wtedy, gdy nie wiemy jakie jest konkretne rozwiązanie danego problemu, a jedynie możemy wskazać które rozwiązanie jest lepsze. No i wszystko się zgadza, prawda? Potrzebujemy od użytkownika wiedzieć jaki chromosom jest dla niego lepszy, jaki ma większą wartość, jaki jest lepiej dopasowany do idealnego, co sprowadza się do funkcji GetFitness. Parametry działania będą potrzebne tylko i wyłącznie nam pod spodem, aby wybrać odpowiednie ścieżki działania algorytmu. Dobra dobra, ale gdzie ta zasada odwracania zależności i jak to się ma do tego wszystkiego?

Zasada odwracania zależności

W sumie równie dobrze mógłbym zacząć projektować bibliotekę od samego dołu, czyli chromosomu i iść w górę – wykorzystywać już to co zrobiłem. No i w tym momencie, Drogi Czytelniku, musimy na chwilę odejść od algorytmów genetycznych i programowania, aby mniej doświadczonym specjalistom dobrze przekazać ideę zasady odwracania zależności.

Na pewno znasz jakiegoś budowlańca. Spoko gość – i cegłę postawi czasem gdzie trzeba, i płytki przez przypadek równo ułoży, i łaciną włada niczym średniowieczny mnich, a i domowej roboty bimbrem czasem poczęstuje! Niczego mu nie brakuje, tylko czy pozwoliłbyś mu decydować o projekcie całego domu, ustawieniu ścian w odpowiednich miejscach i wyborze dachu? No nie sądzę!

Jak to więc powinno wyglądać? Na początku architekt określa jak dom ma wyglądać, jak mają być ułożone pokoje itd, czyli ogólnie mówiąc abstrakcyjnie planuje rozwiązanie problemu. Dalej na kolejnym poziomie układa się już dom z materiału i na końcu przychodzi malarz, który ma za zadanie przyozdobić nam ściany. Złamanie zasady odwracania zależności możemy porównać do sytuacji kiedy malarz decyduje o rozmieszczeniu pokoi, albo gdy projektant wyznacza z imienia i nazwiska kto ma położyć w danym miejscu cegłę. Reguła ta mówi o tym, że szczegóły implementacji nie mogą decydować o abstrakcji, lecz odwrotnie – szczegółowa implementacja zależy od zastosowanej abstrakcji. Analogicznie interfejs czy klasa abstrakcyjna (projektant) nie używa konkretnej klasy (budowlańca), a ogólnie dyktuje zasady korzystając z innych interfejsów czy abstrakcji. Możemy to zobrazować następująco:

class Projektant: IProjektant
{
    void BudujemySciany()
    {
        panMietek.PostawCegle(wspolrzedne.X, wspolrzedne.Y);
    }
}

class PanMietek
{
    void PrzestawiamScianeBoMoge();
}

Projektant zdecydował, że to pan Mietek ma postawić daną cegłę w danym miejscu. Co natomiast będzie jeżeli ten pracownik w danym dniu jest już niedysponowany? Nasz program zwróci błąd, bo nie istnieje instancja danej klasy. Z drugiej strony jeżeli nasz budowlaniec zdecyduje się przestawić ścianę, która została postawiona przez wyższą warstwę projektu, dom (nasza aplikacja) może się zawalić. Kod powinien więc wyglądać następująco:

class Projektant : IProjektant
{
    void UstawSciane()
    {
        Sciana.Lokalizacja = _tamGdziePokazuje;
        IFirmaBudowlana.BudujScianeTutaj(Sciana.Lokalizacja);
    }
}

class PanMietek : IBudowlaniec
{
    void PostawCegle(int x, int y)
    {
        MojaMetodaStawianiaCegiel(x, y);
    }
}

W tym przypadku projektant wskazuje, że w tym miejscu budujemy ściany, a kto zostanie podstawiony pod firmę budowlaną i jak ona to zrobi, już go nie interesuje. Nasz budowlaniec z kolei nie przekracza zakresu swoich możliwości, a implementuje rozwiązanie problemu w jego zakresie odpowiedzialności.


Tym oto sposobem udało nam się zahaczyć jedną z część SOLID’a – zasada odwracania zależności. Mówiąc bardzo krótko chodzi o to, żeby najwyższa warstwa aplikacji nie decydowała o szczegółach implementacji i odwrotnie, żeby będąc na samym dole hierarchii nie zmieniać podstaw projektu. Mam nadzieję, że takie wytłumaczenie przemówiło do Ciebie, bo to oczywiście nie koniec tego typu rozmyśleń w easyGALib. Po więcej zapraszam już wkrótce 🙂

4 Comments

  1. Hej, nie jest to raczej wstrzykiwanie zależności bardziej niz odwracanie?
    Sam kod napisałbym dokładnie tak jak Ty podałeś, czyli uzywanie interfejsów aby była możliwość wstrzyknąc dowolna instancje klasy implementujaca dany interfejs.

    Chodzi mi bardziej o same nazewnictwo, nie wiedziałem ze to sie nazywa odwracaniem zależności.

    „i odwrotnie, żeby będąc na samym dole hierarchii nie zmieniać podstaw projektu. ” jakis przyklad? 🙂

    • Cześć! W zasadzie bardzo często te dwa pojęcia – IoC i DI są wrzucane do jednego worka bez wskazania różnicy. Wstrzykiwanie zależności to raczej dostarczanie z zewnątrz (na różne sposoby) tego co potrzebujemy w środku klasy, aby móc wykonać określone zadanie. W odwracaniu zależności natomiast chodzi o samą relację między komponentami programu, czyli kto i komu karze zrealizować jakąś pracę.
      Przykład: Gość który maluje ściany (szczegółowa implementacja) nie może złapać za fraki projektanta (abstrakcji zarządzające działaniem) i powiedzieć mu że przestawia tą i tą ścianę bo tak chce. W ten sposób abstrakcja zależałaby od szczegółowej implementacji, a zawsze powinno być na odwrót – szczegółowa implementacja ma zależeć od zastosowanej abstrakcji, czyli projektant mówi że w danym miejscu jest ściana, a malarz jedyne co może, to ją pomalować i nie ma nic więcej do powiedzenia, a nawet nie zna projektanta, bo i po co 🙂

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *