Pierwsza część kursu wprowadzająca do tematyki SOLID i świata czystej architektury. Idealna do rozpoczęcia przygody. Podstawy podstaw. Szczypta teorii teoretycznej do bólu. Ale ale… uzasadnienie praktyczne – czemu w ogóle ten SOLID, to tak ważny jest.
Rozmówki o kodzie
Janek: Co to jest programowanie obiektowe?
Seba: Można pisać klasy.
Janek: Jakie znasz wzorce?
Seba: Singleton, Obserwator, MVC
Janek: Wykorzystujesz w praktyce programowanie obiektowe?
Seba: Nie, rzadko. Przecież mam framework.
Taki dialog może być częścią rozmowy rekrutacyjnej. W lekko zmienionej formie, może mieć miejsce przy kanapce w firmowej kuchni. Może też dopaść cię przy kuflu zimnego piwa, podczas wieczornej eskapady społeczności programistycznej.
Kim jest Janek?
Janek Koderek to doświadczony programista i świeżo upieczony Tech Lead. Popełnił wiele złych kodów, ale zdecydowanie więcej napisał tych dobrych. Naczytał się dużo o przeczystym kodzie. Z zamiłowania jest architektem oprogramowania i fanem metodyk zwinnych.
Janek przy pracy.
Na wczoraj? OK. To co z tej listy ma dla ciebie (Product Ownerze) największą wartość biznesową?
Taka kompilacja umiejętności pozwala mu na rzemieślnicze podejście i iteracyjną korektę, nawet jeśli coś w projekcie pójdzie nie tak. Ale o tej historii później.
Co Janek myśli o kodzie i zasadach SOLID? Jak stosuje je w praktyce? Jakiego podstawowego pytania używa, aby torturować Product Ownera, celem wydobycia z niego najbardziej wartościowych informacji?
Zacznij od “dlaczego”
Singleton już nie jest trendy – jest bardzo zły, a w wielowątkowych kodach bywa zabójczy. Bezpieczniej zastosować dependency injection. Obserwator – piszesz go sam? Masz do tego biblioteki. MVC – przecież tylko wypełniasz treścią ciała metod nazwane i narzucone przez framework.
Takie myśli latały po głowie Janka podczas rozmowy, której fragment widziałeś na początku artykułu.
Coraz to nowsze, czasem irracjonalne pomysły zleceniodawcy, zalewają twój kod. To one generują złożoność i prawdziwe wyzwania. Znajomość frameworka to za mało, aby sobie z nimi poradzić.
Jeśli marzysz o tym, aby wypłynąć na ocean głębokiej domeny, to musisz nauczyć się podstaw nawigacji. Aby nie utonąć w nawale klas legacy, musisz zrozumieć i wiedzieć dlaczego tamta metoda jest prywatna, a tu strzelamy interfejsik.
Simon Sinek występując na TED wytłumaczył dokładniej zalety zaczynania od “dlaczego?” https://www.ted.com/talks/simon_sinek…
Zerknij w ramach przerywnika.
Teoria i definicje
… na szczęście w praktyce. Spójrz co zawierają pierwsze notatki Janka ze studiów nad SOLID-em.
Notatki ze studiów
Jak na studiach, ale trochę inaczej bo: zakuj, zalicz i tym razem nie zapomnij 🤓.
- Single Responsibility Principle (SRP) – zasada pojedynczej odpowiedzialności.
- Open-Closed Principle (OCP) – zasada otwarte-zamknięte.
- Liskov Substitution Principle (LSP) – zasada podstawiania Liskov (polimorfizm)
- Interface Segregation Principle (ISP) – zasada segregacji interfejsu.
- Dependency Inversion Principle (DIP) – zasada odwracania zależności.
Postaraj się nauczyć skrótów. Neurony podpowiedzą ci resztę.
Single Responsibility Principle (SRP)
Najpopularniejsza definicja: klasa powinna mieć tylko jedną odpowiedzialność. Definicja alternatywna i najbardziej poprawna: powinien istnieć tylko jeden powód, dla którego klasa może się zmienić.
A w praktyce? Co to jest jedna odpowiedzialność? Czy postępowanie w duchu SRP oznacza, że muszę dzielić kod na setki banalnych klas? Tylko w jakim celu? Istotny jest powód zmiany.
Dawno temu Janek miał w projekcie taki programistyczny, niezdobyty ośmiotysięcznik o nazwie CatalogModule. Robił wszystko. Wyświetlał listę produktów, pojedynczy item, tabelki ze specyfikacjami, ceny. Przykład idealnie złego kodu, niepokryty choćby minimalną warstwą testów. Zdecydowanie powinien być podzielony i zrefaktoryzowany.
Ostateczny kierunek cięcia był jednak inny, niż zespołowi Janka się pierwotnie wydawało. Wystarczyło wydzielić i opisać interfejsem niskopoziomowe operacje na bazie danych. CatalogModule pozostał nadal jedną klasą z kilkunastoma metodami. Różnica polegała na tym, że mieścił się już w granicach pojedynczej warstwy architektonicznej. Czy spełnił założenia SRP? Tak. Będzie dokładnie jeden powód do zmiany. Chęć wprowadzenia innego sposobu prezentacji produktów.
Podział wyglądałby inaczej, gdybyś projektował klasę, przeznaczoną do ponownego wykorzystania. O tym w kolejnym pryncypium.
Open-Closed Principle (OCP)
Otwarte na rozbudowę, a zamknięte na zmianę. Takie twoje klasy być powinny.
A co to znaczy, że klasa jest otwarta na rozbudowę? Przede wszystkim to, że po klasie daje się dziedziczyć. Czyli od strony technicznej, nie jest opatrzona paskudnym słowem kluczowym final. Nie powinna też być nieprzewidywalnym singletonem ze statyczną metodą getInstance.
Klasa koniecznie musi być zgodna z zasadą pojedynczej odpowiedzialności. Dzięki temu nie prowokuje tego, aby dotykać jej wewnętrzny kod.
Klasa może też zachęcać do rozbudowy. Ale już nie przez dziedziczenie, tylko wskutek wewnętrznej elastyczności.Zrealizujesz to przyjmując w parametrach funkcji interfejsy i abstrakcje. I cyk, dzięki polimorfizmowi funkcjonalnie rozszerzyłeś klasę, nawet po niej nie dziedzicząc. 👷
Jak jeszcze zamknąć kod na bezpośrednią zmianę? Chroniąc swoją klasę i kodując defensywnie. Nie rozdawaj zbyt hojnie modyfikatorów public, preferuj raczej private.
Pamiętaj, aby zamknąć się też na nieprzewidziane zmiany w trakcie działania. Buduj stan klasy z parametrów konstruktura, zamiast wystawiać settery. Np. taka metoda setLogger w klasie Application – to proszenie się o problemy.
Liskov Substitution Principle (LSP)
Polimorfizm. Jeśli pies szczeka, to owczarek niemiecki, który jest psem, również szczeka. Tylko inaczej. A jeśli nie szczeka, to niech rzuci wyjątkiem, bo się popsuł. 🐕
Analogicznie sprawa wygląda w przypadku obiektów. Jeśli klasa deklaruje, że rozszerza inną, to tym samym zobowiązuje się spełnić odziedziczony kontrakt.
Akademicki przykład czy Kwadrat jest Prostokątem? Matematyk powie, że tak. Programista odpowie – to zależy. Jeśli zaimplementuje Prostokąt jako obiekt z metodami ustawDługość, ustawSzerokość, to nie może rozszerzyć takiej klasy nazywając ją Kwadrat. Kwadrat co prawda będzie miał metodę ustawDługość, ale co z odziedziczonym długiem – metodą ustawSzerokość?
Pozostawienie metod naruszy zasady matematyki. Nadpisanie i wzajemne ich wywoływanie naruszy zasadę LSP – ustawSzerokość, zgodnie z nazwą, nie powinno dotykać parametru długość.
W kolejnym artykule zobaczysz dwa nieoczywiste, ale powszechnie pojawiające się naruszenia zasady podstawiania.
Interface Segregation Principle (ISP)
Kiedy interfejs jest dobry? Gdy mówi prawdę, zgodną ze stanem faktycznym. Nie dopuszczaj, aby nazywał się IMessageSender, a przy okazji zapisywał coś do pliku. To by było kłamstwo.
Dobry interfejs musi być przydatny. Nie może się nazywać IDoAnything, to czyni go bezużytecznym. Powinien być maksymalnie specjalizowany.
W kuchni, podczas rozmowy w nowym developerem, Janek do wytłumaczenia idei interfejsów użył poniższej analogii.
Opisując nawyki żywieniowe człowieka możesz machnąć interfejsik IOmnivore (ang. wszystkożerny). To by była prawda. Ale tylko dla części obiektów. Elastyczniej będzie posiadać dwa interfejsy IMeatEater 🥩 i IVegeLover 🍎🥬. Przecież nie zmusisz wszystkich instancji klasy człowiek do jedzenia mięsa.
Dzieląc zachowania na deskryptywne interfejsy, uwalniasz klasy dziedziczące od implementowania zbędnych metod, które wewnątrz będą miały komentarz /* do nothing */ albo instrukcję pass. Tym samym zwiększasz prawdopodobieństwo ponownego użycia, czyli po prostu przydatność twojego kodu.
Dependency Inversion Principle (DIP)
Czyli zasada mówiąca, że kierunek zależności w kodzie powinien być odwrotny do kierunku przepływu sterowania w aplikacji. ↩️
Przepływy z prądem i pod prąd
W kodzie każdej aplikacji zauważysz dwa przepływy.
- Przepływ sterowania czyli: Aplikacja → Metoda kontrolera → Fabryka → Usługa
- Przepływ zależności, to z kolei relacje: Interfejs usługi ← Konkretna implementacja usługi oraz Fabryka abstrakcyjna ← Konkretna implementacja fabryki
Przypomnę standardowe pytania Janka: dlaczego? W jakim celu zależności w kodzie muszą mieć przeciwny zwrot niż kierunek przepływu sterowania?
Twój wymarzony dom
Planujesz budowę domu 🏠. W myślach rysujesz rozkład pokojów. Wyobrażasz sobie fotel obok kominka i stół przy oknie. Na tym etapie abstrakcji nie interesuje cię, jaki to będzie fotel. Ma być po prostu wygodny. Stół – ma po prostu być stołem.
Spróbuj zamodelować w kodzie zależności opisujące dom. WykańczanieDomu to twoja aplikacja, umeblujSalon to metoda kontrolera. Jakaś fabryka wyprodukowała fotel, zdolny z twoją specyfikacją. Fotel pozwala na nim wygodnie usiąść. Tak! To przepływ sterowania.
Czy dostrzegasz przepływ zależności w kodzie? Fabryka IDEA zgodnie z abstrakcją opisującą fabrykę produkuje fotel. Model fotela Grubbo dostarcza usługę siedzenia, zgodną z interfejsem wygodneSiedzenieNaFotelu.
Co zyskujesz? Nie jesteś uzależniony od konkretnego producenta i modelu (usługi). W dowolnym momencie, nie naruszając konceptu twojego domu (przepływu sterowania), możesz wymienić elementy składowe (zależności w kodzie). Tym samym osiągasz projekt zgodny z zasadą od ogółu do szczegółu.
Obrazkowe podsumowanie zasady DIP
Na poniższym schemacie klas zobaczysz wszystkie elementy zasady DIP:
- granicę, która oddziela abstrakcje od konkretnych implementacji (bladoczerwona linia)
- kierunek przepływu sterowania od aplikacji do konkretnych implementacji (strzałki wychodzące od komponentu Application)
- niezależność aplikacji od konkretnej implementacji (fabryki)
- zależności konkretnych implementacji od interfejsów (zamknięte niewypełnione strzałki)
Co zrobić z tak potężną wiedzą?
Notatka podsumowująca
- Single Responsibility Principle (SRP) – bądź minimalistą, niech napisana przez Ciebie klasa ma dokładnie jeden powód do zmiany. Klasy Misc-, Utils-, Tools- są złem 👿.
- Open-Closed Principle (OCP) – programuj defensywnie, używaj prywatnych modyfikatorów dostępu, zrezygnuj z publicznych getterów i setterów. Nie używaj słowa kluczowego final. Pozwól na rozbudowę przyjmując w parametrach metod publicznych abstrakcyjne klasy albo interfejsy.
- Liskov Substitution Principle (LSP) – daj sobie i innym, możliwość dziedziczenia klas zgodnie z kontraktem definiowanym w klasie bazowej.
- Interface Segregation Principle (ISP) – znowu minimalizm. Grupuj i specjalizuj kod tak, aby implementacja interfejsu czy dziedziczenie po klasie abstrakcyjnej, nie implikowało pisania metod void, które wewnątrz będą miały komentarz /* do nothing */.
- Dependency Inversion Principle (DIP) – nie wiąż się z konkretnymi implementacjami, tylko z abstrakcjami i zachowaniami. To droga do architektonicznego jednorożca – idealnie stabilnego, ale łatwo modyfikowalnego komponentu wysokiego poziomu
Funkcja trackback/Funkcja pingback