S.O.L.I.D.ne programowanie – część 0, czyli wstęp
S.O.L.I.D.ne programowanie – część 1, czyli monogamia
S.O.L.I.D.ne programowanie – część 2, czyli spoufalamy się
S.O.L.I.D.ne programowanie – część 3, czyli podkładamy świnię
S.O.L.I.D.ne programowanie – część 4, czyli apartheid

Pomysł na codzienne pisanie na blogu ma pewne zalety. Na przykład można dokończyć coś co pisałem… 7 lat temu. Dziwne uczucie, ale wiecie co? Bardzo mi się podoba. Zatem zapraszam do ostatniej piątej części z cyklu S.O.L.I.D.ne programowanie gdzie przyjrzymy się zasadzie odwrócenia zależności (Dependency Inversion).

Przepraszam, ale kto tu pana wpuścił

W książce „Podróż bez biletu” Władysław Kisielewski opisuje zdarzenie jakie miało miejsce już na terenie jednej z baz w Wielkiej Brytanii. Otóż w polskim lotnictwie działającym w strukturach RAF obowiązywała podwójna szarża. Pierwsza polska oraz druga brytyjska. Jeden z pilotów miał pecha i wg. polskiej szarży był podoficerem, a według brytyjskiej oficerem (albo na dwrót, nie mam książki przy sobie i nie sprawdzę). Wynikały z tego różne dziwne sytuacje, których zwieńczeniem było niewpuszczanie go do kantyn. Do oficerskiej nie wpuszczano podoficera, do kantyny szeregowych oficera. Trochę problem, bo jeść trzeba, a nie ma gdzie. Jest to przykład tego jak uzależnienie abstrakcji (przysługuje posiłek) od implementacji (stopnia) powoduje problem.

Ostatnia z zasad SOLID tyczy się właśnie tego jak należy postępować z zależnościami pomiędzy abstrakcją, a implementacją. Sama zasada jest bardzo prosta na poziomie… abstrakcji:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

Wszystko zdaje się jasne. Rzecz w tym, że nie do końca.

Warstwy jak w torcie cebulowym

W klasycznej, dobrze napisanej, aplikacji mamy do czynienia z rozdzieleniem odpowiedzialności pomiędzy poszczególnymi elementami. Mówią o tym poprzednie cztery zasady SOLID. Należy zatem dbać by klasy miały jedną odpowiedzialność (SRP), implementacje przestrzegały kontraktu (OCP, LSP), a i rozdzielenie zadań w ramach odpowiedzialności nie jest głupim pomysłem (ISP). Wszystko ładnie pięknie, ale jest jeden problem…

Mamy sobie naszą piękną SOLI-dną apkę, w której gęsto występują jednak zależności pomiędzy bardzo wysokopoziomowymi elementami, a niskopoziomową implementacją. Nawet wprowadzenie interfejsów nie ratuje sytuacji ponieważ nadal musimy jakoś tworzyć obiekty. To znowuż uzależnia nasze warstwy w bezpośredni sposób.

Po co nam managerowie średniego szczebla?

Kto to jest mendager średniego szczebla? Można powiedzieć, że to stanowisko, które utrzymujemy w firmie by w ramach społecznej odpowiedzialności biznesu zadbać o tych, którym grozi strukturalne bezrobocie. W rzeczywistości ich praca polega na zbieraniu, analizowaniu i przekazywaniu informacji o postępach wyżej. Oznacza to, że stanowią oni warstwę pośrednią pomiędzy pracownikami (niskopoziomową implementacją), a zarządem (wysokopoziomową abstrakcją). Prezes w dużej firmie powinien wiedzieć co robi, ale nie musi umieć klikać w jakimś tam excelu o trzydziestoznakowej sygnaturze albo rzeźbić zębatek na tokarce dolnowrzecionowej. Co więcej nie powinien też zajmować się duperelami w rodzaju zamawiania frezów do tejże tokarki. To jest zadanie przeznaczone dla managerów średniego szczebla. Wiedzieć jak dokładnie działa proces. Koordynować pracę ludzi oraz zapewniać im zasoby.

Przemysł nienawiści

Skierowanej przeciwko konkretnym implementacjom. Zbudowany w oparciu o wzorzec fabryki, metody fabrykującej albo innego wzorca kreacyjnego. Napędzany megabajtami kodu z frameworków dependency injection i łzami developerów przeszukujących SO w poszukiwaniu rozwiązań problemów z konfiguracją tychże frameworków. Jednym słowem w każdej apce, w której masz więcej niż dwie klasy warto zastanowić się nad wprowadzeniem DI w rodzaju springa w celu tworzenia nowych obiektów. Jednak to nie wszystko. Dependency inversion to nie tylko wdrożenie kontenerka DI, ale przede wszystkim przestrzeganie kilku zasad w tworzeniu samego kodu. Oto i one.

All member variables in a class must be interfaces or abstracts

Czyli po ludzku jeżeli klasa ma pole to typ musi być interfejsem albo klasą abstrakcyjną. Dzięki temu tworzymy zależności pomiędzy klasami na poziomie interfejsów rozumianych jako zbiór zachowań, a nie implements. Dzięki temu możemy korzystając z OCP i LSP wymieniać implementacje bez zwracania uwagi na szczegóły. Oczywiście życie to nie je bajka i jeżeli chcemy zachować taką elastyczność to należy zastanowić się nad jakimś mechanizmem konfiguracji dla poszczególnych implementacji. Zatem problem przepychamy z kodu do zarządzania konfiguracją. To już jest, trochę, prostsze.

All concrete class packages must connect only through interface/abstract classes packages.

To jest naprawdę fajna reguła. Definiuje ona w jaki sposób należy budować strukturę projektu. Jeżeli znamy prezentację Wujka Boba o Clean Architecture to łatwo skojarzymy tą zasadę z zasadami jakie rządzą komunikacją interaktorów. Gadamy tylko po interfejsach.
Dodatkowym bonusem do tej zasady jest możliwość zaprzęgnięcia statycznej analizy kodu w celu poszukiwania naruszeń. Dzięki temu mamy finalnie całkiem przyjemny w utrzymaniu kod.
Niestety nie wszystko zołto* co się świeci. Nie każdemu będzie pasować konstrukcja z pakietami zawierającymi same interfejsy i podpakietami impl, a to jest najprostsza droga by nie bruździć sobie w kodzie.

No class should derive from a concrete class.

Słowo klucz derive, które oznacza tu „czerpać”. Innymi słowy jeżeli chcesz coś zrobić z jakimś obiektem to nie pakujesz mu się bezpośrednio z butami do metod, ale elegancko przez interfejs. Zależność działa też w drugą stronę, bo derive oznacza też „wywodzić”. Zatem metody publiczne nie mogą zwracać konkretnych klas, a jedynie interfejsy.

No method should override an implemented method.

Kwintesencja projektowania zgodnego z DIP. Jeżeli tworzymy hierarchię klas to należy unikać konstrukcji w stylu:

Listing 1. Tak nie robimy

class A{

    public void method(){
       //...
    }
}

class B extends A{

    public void method(){
       super.method();
       //...
    }
}

Inaczej mówiąc jeżeli chcemy nadpisać coś co zostało już zaimplementowane na wyższym poziomie to znaczy, że w naszej hierarchii mamy jakiś błąd. Naprawienie jest trudne, ale możliwe. Z pomocą przychodzą nam metody szablonowe, delegacja oraz dekoratory.

All variable instantiation requires the implementation of a Creational pattern as the Factory Method or the Factory pattern, or the more complex use of a Dependency Injection framework.

O tym była już tu mowa. Nasza aplikacja będzie musiała koniec końców zatrudnić jakiegoś „managera średniego szczebla”, tylko po to by zarządzać wzajemnymi relacjami pomiędzy obiektami. Ma to sens ponieważ pozwala na odseparowanie poszczególnych kawałków kodu i tym samym łatwiejsze ich utrzymanie.

Podsumowanie

Trwało to 7 lat. I przez ten czas wiele się nauczyłem. To o czym pisałem w całym tym cyklu miałem okazję wielokrotnie stosować na żywym organizmie. Czasami wychodziło lepiej czasami nie całkiem dobrze, ale nie sparzyłem się ani razu na tych zasadach. O ile oczywiście ich przestrzegałem.

* niski wzrost, skłonność do irytacji, pamiętajcie, że literówki mogą zaboleć