Zaawansowana część cyklu o zasadach SOLID: odsłona druga. Wiesz jak prawidłowo grupować klasy wewnątrz komponentów. Teraz pora na zasady skutecznego łączenia ich ze sobą. Będzie o tym jak zachować zdrowe relacje, a na tych niezdrowych zaaplikować właściwą terapię.
W poprzedniej części wprowadziliśmy odrobinę teorii czym są zasady SOLID w odniesieniu do komponentów. Następnie skupiliśmy, się jak projektować komponenty, czyli w skrócie zapewniać właściwą koherencję klas (spójność) wewnątrz nich.
Zasady Łącz i działaj
Pora wprawić maszynerię w ruch, ucząc się jak budować relację między komponentami.
Żaden komponent nie jest samoistną wyspą: każdy stanowi ułamek aplikacji, część ekosystemu.
Luźno, na podstawie cytatu z Ernesta Hemingwaya – Komu bije dzwon
Mozartify
Tomek, właściciel startupu Mozartify, z zadowoleniem patrzył na wykres pokazujący przyrost nowych użytkowników usługi. Wszystko dzięki Jankowi, który wraz ze swoim zespołem, w ekspresowym tempie zaimplementował niezbędny moduł subskrypcji.
Tomek wpadł na pomysł, aby spersonalizować ofertę dla obecnych użytkowników i wprowadzić okresowe promocje. Czyli atrakcyjny program lojalnościowy, polegający na ekstra punktach (biciach serca) dodawanych po przedłużeniu subskrypcji.
Janek zebrał wymagania i z zespołem zaczęli szkicować architekturę nowego rozwiązania.
Punkt wyjściowy
Pozwól, że przypomnę ci w telegraficznym skrócie układ katalogów i komponenty projektu.
Mozartify składa się z trzech core’owych komponentów. Oto one, wraz z jednolinijkowym opisem ich odpowiedzialności:
- Mozartify\Subscription – pakiety subskrypcji usługi, zasady ich kupowania
- Mozartify\Marketplace – przygotowanie oferty dla nowych i istniejących subskrybentów
- Mozartify\Player – techniczne odtwarzanie uwzględniające innowacyjne rozliczanie usługi.
Acyclic Dependencies Principle (ADP)
Gdzie umieścić zasady programu lojalnościowego? Na pewno nie w module subskrypcji. Pamiętasz, że zespół Janka celowo “wyrzucił” przygotowanie oferty do oddzielnego komponentu, celem zachowania zgodności z zasadą Common Reuse Principle.
Dobrym pomysłem wydaje się umieszczenie regułek lojalnościowych w komponencie Marketplace.
Pierwsze podejście do programu lojalnościowego
Program lojalnościowy wpływa na parametry subskrybowanego pakietu. W zależności od stażu, z każdym kolejnym pakietem, przyznaje bonusowe punkty.
Czyli osiągamy coś takiego jak na powyższym obrazku. Przyjrzyj się uważnie zależnościom. Czarno na białym widać, że zasady subskrypcji zależą od marketplace.
Cykle w zależnościach komponentów to zło
Efektem umieszczenia LoyaltyRules w komponencie Marketplace jest powstanie cyklu. Jakie są tego skutki?
- Zmiana w komponencie Subscription, będzie wymagała przygotowania nowej wersji Marketplace (konieczność dostosowania metody prepareOffer)
- Zmiana w komponencie Marketplace, będzie wymagała przejrzenia i ewentualnej adaptacji kodu metody buyPackage.
Brzmi niegroźnie? Dodaj do wszystkiego mikroserwisowe opakowanie i komunikację po HTTP. Albo po prostu przypadek, kiedy każdy z komponentów utrzymuje inny zespół programistów. Co z kolei oznacza:
- wspólne review
- konieczność wdrożenia w tym samym momencie
- czyli konieczność synchronizacji i zachowania spójności
- lub zapewnienie kontraktu wstecz, a później usunięcie zbędnego kodu.
Grafy acykliczne w relacjach między komponentami
Janek wiedział, ze potrzebuje lepszego rozwiązania. Postanowił rozerwać łańcuch zależności i uzyskać drogi acykliczne w relacjach komponentów.
Graf acykliczny to graf niezawierający drogi zamkniętej
Wikipedia: https://pl.wikipedia.org/wiki/Graf_(matematyka)
Co powiesz na poniższy schemat relacji między klasami komponentów? Zobacz jak teraz wygląda cykl zależności między Subscription a Marketplace. Widzisz ten cykl? Nie widzisz? To gitara, bo już go nie ma!
Janek zastosował tu odwrócenie zależności (DIP). Komponent Subscription definiuje abstrakcje opisujące, w jaki sposób naliczane są bicia serca (HeartbeatPolicy), natomiast Marketplace posłusznie dostarcza konkretną implementację. Komponent Subscription ponownie nie jest zależny od Marketplace. O to właśnie chodziło 🍾.
Wydzielenie komponentu
Drugi pomysł, który przyszedł Jankowi do głowy, to budowa nowego komponentu HeartbeatPolicy. Dzięki temu:
- nadal zachowana jest acykliczna droga między klasami komponentów Subscription i Marketplace
- komponenty Subscription i HearbeatPolicy mają jasno określone odpowiedzialności (zgodność z Common Reuse Principle)
Pojawia się jednak pewne zagrożenie. Wprowadzenie zmiany w HeartbeatsPolicy doprowadzi do konieczności przejrzenia kodu komponentów od niego zależnych: Subscription i Marketplace. Jest na to pewne rozwiązanie zahaczające o zagadnienia stabilności i abstrakcyjności. I już za moment je poznasz.
Stable Dependencies Principle (SDP)
Spacerujesz ulicą i widzisz ogromny dąb. Pomijając zgody formalne, fizycznie ciężko go wyrwać. Stabilny skubaniec, rośnie tu długo i ma mocne korzenie. Co się stanie jak będziesz go wyrywał? Ile rzeczy “związało” z nim swój los? Pewnie pociągnie za sobą spory kawałek obsadzonej roślinami ziemi, uszkodzi chodnik, kostkę na podjeździe, fragment asfaltu pobliskiej jezdni.
Teraz wyobraź sobie komponent. Istnieje od pradziejów w projekcie. Gdzie się nie obejrzysz, prawie wszystko z niego korzysta. Podejmiesz się usunięcia go z projektu? Odważysz się przebudować kod? Będzie trudno. Czyli im większa stabilność, tym większa trudność w pozbyciu się komponentu.
Po drugiej stronie jest inny komponent. Posiada tylko jedną relację przychodzącą (coś tam z niego korzysta). Reszta to relacje wychodzące, czyli odwołania do klas z innych komponentów. Definiując niestabilność jako łatwość usunięcia komponentu powiemy, że zdecydowanie jest on bardzo niestabilny.
Miara niestabilności
Jak najprościej liczbowo opisać niestabilność komponentu?
I = Out / In + Out
I (instability) – metryka niestabilności badanego komponentu
Out – wychodzące relacje, czyli od czego zależą klasy badanego komponentu
In – przychodzące relacje, czyli jakie klasy (komponenty) zależą od (klas) badanego komponentu
Case study przed wydzieleniem komponentu HeartbeatPolicy
Przeanalizujmy metryki niestabilności (I) komponentów Mozartify, zanim jeszcze pojawiły się regułki programu lojalnościowego.
Metryka niestabilności | Interpretacja |
I(Subscription) = 0/2 = 0 | Zerowa niestabilność czyli komponent najbardziej stabilny. Bardziej być nie może. |
I(Marketplace) = 1/2 | Takie pół na pół. Trochę od czegoś zależy, a trochę uzależnia. |
I(Web) = 2/2 = 1 | Niestabilność równa jeden, to maksymalna wartość metryki. Nic nie uzależni się od tak opisanego komponentu. |
Zrobię pewną żonglerkę kodem, abyś mógł popatrzeć na drugi przypadek.
Case study po wydzieleniu komponentu HeartbeatPolicy
Czy metryki niestabilności (I) zmieniły się po dodaniu komponentu HeartbeatPolicy? Zdecydowanie! Subscription nie jest już najbardziej stabilnym komponentem.
Spójrz na tabelkę poniżej. Zobaczysz jak bardzo zmiana wpłynęła na połączenia komponentów.
Metryka niestabilności | Interpretacja |
I(HeartbeatPolicy) = 0/2 = 0 | Zmiana lidera w konkursie na najbardziej stabilny komponent. Zerowa niestabilność, czyli komponent od niczego nie zależy. |
I(Subscription) = 1/3 | Po zmianie komponent subskrypcji, zależy od HeartbeatPolicy. |
I(Marketplace) = 2/3 | Marketplace uzależnił się od HeartbeatPolicy i Subscription. Relacja przychodząca z komponentu Web zwiększa jego stabilność. |
I(Web) = 2/2 = 1 | Nadal najmniej stabilny komponent. |
Inwestuj w dobre relacje
Zanim zdefiniujemy dobrą zasadę budowania relacji komponentów, popatrz raz jeszcze na schemat “Niestabilność po wydzieleniu komponentu HeartbeatPolicy”. Są tam takie małe szczególiki – groty strzałek. Pokazują relację “zależę od X”.
Szukaną “X” można uogólnić, osiągając ostateczna wersję zdania: “zależę od komponentu bardziej stabilnego niż ja sam”.
Uzależniaj się od komponentów bardziej stabilnych niż ten, który piszesz. Komponenty stabilne zmieniają się rzadko. Dlaczego? Ponieważ trudno je zmienić. Jeśli trudno je zmienić, to bardzo małe jest prawdopodobieństwo, że pojawi się zmiana, która przełoży się na konieczność adaptacji twojego komponentu.
Przykład. Popatrz na biblioteki standardowe języków programowania. Kojarzysz nagminnie pojawiające się sytuacje, w których musiałeś zmieniać swój kod, bo pojawiła się w nich jakaś zmiana?
Tylko że… w przypadku HeartbeatPolicy chcielibyśmy mieć możliwość zmiany reguł. Czy organizując komponenty opisany sposób, utrudnimy sobie wprowadzanie zmian w przyszłości? Tak, jeśli nie wykonamy jeszcze jednego istotnego kroku.
Stable Abstraction Principle (SAP)
Pozwól, że wprowadzę pojęcie stabilnej abstrakcji i abstrakcyjnej stabilności, czyli definicję komponentu idealnego. Zanim jednak dotrzemy do definicji, zatrzymajmy się na moment przy abstrakcyjności.
W mega uproszczeniu, abstrakcje w programowaniu obiektowym, to nic innego jak interfejsy i klasy abstrakcyjne. Przy czym nie jest konieczne, aby używany język programowania definiował słowa kluczowe abstract i interface. Abstrakcja to nazwanie zbioru cech i zachowań, a później operowanie już tą „abstrakcyjną” nazwą. To pozwala skupić się na algorytmie przetwarzania, bez wchodzenia w najdrobniejsze szczegóły implementacyjne.
Miara abstrakcyjności
Jest sobie taki oto wzór:
A = Na / Nc
A – metryka abstrakcyjności
Na – liczba interfejsów i klas abstrakcyjnych w komponencie
Nc – całkowita liczba wszystkich klas i interfejsów w komponencie.
W celu zrozumienia, do czego przyda się metryka abstrakcyjności, musisz spojrzeć na wykres zależności niestabilności(I) od abstrakcyjności (A) komponentu.
Komponenty bezużyteczne i trudne
Jeśli komponent jest
- bardzo abstrakcyjny,
- ale też niestabilny (czyt. nic albo niewiele od niego zależy)
to znaczy, że w praktyce jest nic z niego nie korzysta = jest bezużyteczny.
Jeśli komponent jest
- w niewielkim stopniu abstrakcyjny,
- ale bardzo stabilny (czyt. wiele innych komponentów opiera o niego swe działanie)
to znaczy, że będzie trudno, bardzo trudno dokonać zmiany w jego kodzie.
Takie komponenty są niezmiernie niepożądane i z chęcią je pożegnamy.
Komponenty idealne
Nasza podróż przez świat komponentów, zmierza do definicji ostatecznej. Komponent idealny powinien być tak abstrakcyjny jak jest stabilny.
Wracając na moment do interpretacji graficznej – im bliżej niebieskiej linii się znajduje, tym lepiej.
Posłużmy się przykładem HeartbeatPolicy. Policzyliśmy jego niestabilność (I) otrzymując wartość równą 0 (zero). Czyli jest ekstremalnie stabilny, wiele miejsc w projekcie z niego korzysta. Biznesowo zależy nam na plastyczności takiego komponentu. Chcemy, aby pozwalał na łatwe wprowadzanie modyfikacji.
Wiesz już, że otwartość na rozbudowę, bez jednoczesnych modyfikacji kodu, osiągniesz stosując interfejsy (abstrakcje). Teraz wystarczy, że skompilujesz całą zdobytą wiedzę i zapewnisz, że odwołania do HeartbeatPolicy w komponentach Marketplace i Subscription, będą opierać się o te abstrakcje.
Dobrym przykładem są też biblioteki dostępowe do bazy danych. Są to komponenty bardzo stabilne, wykorzystywane w mnóstwie miejsc w kodzie. A jednak są łatwe do modyfikacji. Dodanie nowego silnika bazodanowego, nie pociąga za sobą wielkiej przebudowy. Wykorzystanie bilbioteki ukryte jest elegancko za abstrakcjami.
Podsumowanie
No i koniec 😔. To ostatni artykuł cyklu “Zasady SOLID w czystej architekturze”.
- Poznałeś zasady solid i wiesz, dlaczego są tak ważne.
- Na przykładzie nauczyłeś się praktycznego solidnego kodowania.
- Dowiedziałeś się jak prawidłowo grupować komponenty i dopasować je do dynamiki zmian w projekcie.
- Odkryłeś jak tworzyć dobre relacje między komponentami
Cieszę się 😀, jeśli “Zasady SOLID w czystej architekturze” spowodowały, że:
- nabrałeś odwagi i twoja przygoda z SOLID właśnie się rozpoczyna
- poukładałeś swoją wiedzę, dzięki czemu kodowanie w duchu SOLID będzie bardziej świadome
- przekonałeś się, aby stosować pochodne SOLID w szerszym kontekście niż pojedyncze klasy
- zdobyłeś podstawy nawigacji w zagadnieniach grupowania i łączenia komponentów.
To nie koniec. To dopiero początek 🚀.
Dziękuję za twoje dzielne dotrwanie do tego akapitu 💪.
PS. Masz pytanie? Wal śmiało 🥊: w komentarzu, na fanpage lub przez messenger migawka.it. Wolisz kontakt bezpośredni? Namiary na moje kanały w social media znajdziesz tutaj.
Powodzenia i do zobaczenia!
Michał
Funkcja trackback/Funkcja pingback