Jak pisać SOLIDny kod?

21.11.2017 AUTOR: Adam Matysiak

Zastanówmy się jak stwierdzić czy napisany przez nas kod jest dobrej jakości? Czy spełnia standardy? Czy będzie go łatwo rozwijać w przyszłości? O ile mamy sporo narzędzi, które mogą nam powiedzieć czy kod działa według założeń, to zdecydowanie mniej, które nam powiedzą czy kod jest dobrej jakości. A przy okazji są trudniejsze w zrozumieniu (jak np. złożoność cyklomatyczna).

Skoro narzędzi jest mało i są trudne w rozumieniu (w przeciwieństwie do zero-jedynkowych testów jednostkowych) to może warto się trzymać kilku zasad pisania kodu obiektowego, by był bardziej zrozumiały, elastyczny i łatwiejszy w utrzymaniu?

Nie ma wątpliwości wśród programistów, że najważniejszym zbiorem dobrych zasad są zasady zawarte w akronimie SOLID:

  • S – SRP – Single responsibility principle (Zasada jednej odpowiedzialności)
  • O – OCP – Open/closed principle (Zasada otwarte-zamknięte)
  • L – LSP – Liskov substitution principle (Zasada podstawienia Liskov)
  • I – ISP – Interface segregation principle (Zasada segregacji interfejsów)
  • D – DIP – Dependency inversion principle (Zasada odwrócenia zależności)

Omówmy pokrótce wszystkie 5 zasad.

SRP – Zasada jednej odpowiedzialności

Definicja SRP to:
Klasa powinna mieć tylko jedną odpowiedzialność, więc nigdy nie powinien istnieć więcej niż jeden powód do modyfikacji klasy.

SRP to najważniejsza zasada ze wszystkich, ale też najtrudniejsza w realizacji. Każdy z nas w przeszłości napisał kod klasy, która robiła wiele rzeczy (przyjmowała dane, analizowała, zapisywała do bazy danych, zmieniała swój stan i zwracała sformatowaną odpowiedź).

Dla przykładu załóżmy, że mamy klasę, która analizuje dane do raportu i generuje PDF z tymi danymi. Intuicyjnie chcielibyśmy wszystko zmieścić w jedną klasę, by kod był spójny. Jednakże klasa ma dwie odpowiedzialności: przetwarzanie danych i generowanie PDF, a więc istnieją dwa powody, dla których klasa mogłaby być edytowana.

I tego właśnie chce uniknąć ta zasada. Także jak rozwiązać nasz przykład? Najbardziej oczywistym wydaje się wykorzystanie wzorca projektowego Dekorator. To rozwiązanie wymaga posiadania dwóch klas: pierwszej, która przetwarza dane do raportu; oraz drugiej, która generuje raport na podstawie obiektu klasy pierwszej.

Innym rozwiązaniem wydaje się wstrzyknięcie zależności generującej pdfy do klasy pierwszej. Niestety to podejście może naruszać SRP, jeśli będziemy musieli dokonać bardziej gruntownych zmian w kwestii generowania raportów.

OCP – Zasada otwarte/zamknięte

Definicja OCP to:
Klasy powinny być otwarte na rozszerzenia i zamknięte na modyfikacje.
 
Zasada ta zachęca do wykorzystania polimorfizmu, dzięki czemu nie będzie problemu z dodawaniem rozszerzeń (nowych klas) do kodu, jednocześnie nie musząc modyfikować istniejącego już kodu.

Dla przykładu załóżmy, że mamy klasy reprezentujące różne figury: trójkąt, kwadrat i koło, a także klasę, która oblicza sumę ich powierzchni. Prostym rozwiązaniem byłaby sekwencja ifów sprawdzająca typ klasy-figury i obliczająca powierzchnię, jeśli warunek byłby spełniony.

Czy klasa jest otwarta na rozszerzenia? Tak, nie ma problemu by dodać kolejny if, jeśli chcielibyśmy dodać obsługę pięciokątów. Ale czy klasa jest zamknięta na modyfikację? Nie, bo musimy dodać kolejny if i kod obliczający powierzchnię wewnątrz istniejącej już klasy.

 

I w tym momencie z pomocą przychodzą nam interfejsy i polimorfizm. Wszakże każda klasa-figura może zawierać u siebie metodę do obliczania powierzchni, a także implementować interfejs, zawierający właśnie tę metodę. Wtedy do klasy obliczającej powierzchnię przekazujemy tylko obiekty implementujące interfejs, a to już te obiekty obliczają swoją powierzchnię. Klasa obliczająca powierzchnię po prostu sumuje wartości.

Teraz nasza klasa dalej jest otwarta na rozszerzenia – nie ma problemu by stworzyć nowe klasy, które będą implementować interfejs, ale jednocześnie jest zamknięta na modyfikacje, więc dodanie nowej klasy-figury nie powoduje potrzeby zmiany kodu w istniejących klasach.

LSP – Zasada podstawienia Liskov

Definicja LSP to:
Obiekty powinny być zastępowalne przez instancje ich podtypów bez wpływu na poprawność kodu.

LSP to najtrudniejsza w zrozumieniu zasada, ale wystarczy dobry przykład, by rozwiać wątpliwości.

Załóżmy, że mamy klasy reprezentujące ogólną klasę Pojazdu, a także dziedziczące po niej klasy Samochód i Ciężarówka. Zasada Liskov mówi o tym, że jeśli Samochód dziedziczy po Pojeździe (relacja is-a) to nie powinno mieć znaczenia dla poprawności programu czy w tym miejscu podstawimy ogólną klasę Pojazd, czy konkretną Samochód albo Ciężarówka. Nadal możemy obliczyć np. koszt lub prędkość.

Innymi słowy, jeśli jakaś klasa dziedziczy po innej, to podklasa nie powinna zmieniać zachowania klasy nadrzędnej.

ISP – Zasada postawienia interfejsów

Definicja ISP to:
Wiele dedykowanych interfejsów jest lepsze niż jeden ogólny.

Definicja może za dużo nie mówi, ale to jest również prosta zasada. Chodzi o to, by interfejsy, po których dziedziczą klasy, były jak najbardziej konkretne i nie zmuszały klasę do implementowania metod, z których nie będzie korzystać. Czyli lepiej zrobić dwa interfejsy z jedną metodą, jeśli nie wszystkie klasy będą musiały korzystać z obu metod; w przeciwieństwie do sytuacji, gdybyśmy mieli jeden interfejs z dwoma metodami, a nie każda klasa musiałby korzystać z obu.

Powróćmy do przykładu z figurami. Żeby spełnić zasadę OCP, zapewne stworzyliśmy interfejs do wyliczania powierzchni. Jeśli do tego samego interfejsu dodalibyśmy metodę do wyliczania objętości to klasy jak graniastosłup i kula nie naruszałyby ISP, ale klasy, które już wcześniej mieliśmy – kwadrat, trójkąt i koło już by naruszały, jako że nie mają objętości. Dlatego lepiej byłoby stworzyć dwa interfejsy – jeden, który pozwala obliczać powierzchnię; oraz drugi, który pozwala obliczać objętość. Nic nie stoi na przeszkodzie by kwadrat implementował pierwszy z nich, a sześcian oba.

DIP – Zasada odwrócenia zależności

Definicja DIP to:
Wysokopoziomowe moduły nie powinny zależeć od modułów niskopoziomowych – zależności między nimi powinny wynikać z abstrakcji.

Niestety znowu definicja nie jest banalnie prosta, ale zasada znowu nie jest trudna. Zasada ta zachęca do luźnego wiązania klas między sobą (ang. decoupling). Zatem jedna klasa nie powinna zależeć od drugiej, ale od abstrakcji, czyli interfejsu, który ta klasa implementuje. Dzięki temu nie będzie problemu by podmienić wstrzykiwany obiekt innego typu, jeśli implementuje ten sam interfejs.

Spójrzmy na przykład z klasą, która otrzymuje w konstruktorze obiekt połączenia z bazą danych MySQL by wykonać na niej jakąś operację. Jeśli w przyszłości będziemy chcieli zmienić bazę danych, będziemy musieli naruszyć zasadę OCP, czyli nasza klasa nie jest zamknięta na modyfikację. Natomiast jeśli nasza klasa miałaby przekazywany obiekt implementujący interfejs DBConnection to dla naszej klasy nie ma znaczenia z jakiej bazy danych korzystamy. Ważne są tylko metody interfejsu (abstrakcji), a nie niskopoziomowego modułu.

Podsumowanie

O tych 5 złotych zasadach można pisać bardzo dużo, ale najważniejsza jest praktyka. Niestety w praktyce często będziemy się zmagać z problemami w uzyskaniu kodu zgodnego z SOLID, szczególnie z pierwszą zasadą. Powiem więcej – kontrolery we wzorcu MVP łamią SRP, gdyż odpowiadają i za autoryzację, i za walidację, i za przetwarzanie danych i za odpowiedź. Jak widać – nie można mieć wszystkiego ;). Ale to jest specyficzny wyjątek.

Zawsze powinniśmy się starać by nasz kod był SOLIDny. I jeśli teraz o to nie zadbamy, to w przyszłości będziemy mieli przez to problemy. Dlatego gorąco zachęcam do wzięcia sobie tych 5 zasad do serca i kierowania się nimi w swojej pracy.

Powodzenia!

Artykuł powstał dzięki:

Coders Lab

Łącząc doświadczenie edukacyjne ze znajomością rynku pracy IT, Coders Lab umożliwia szybkie i efektywne zdobycie pożądanych kompetencji związanych z nowymi technologiami. Skupia się się na przekazywaniu praktycznych umiejętności, które w pierwszej kolejności są przydatne u pracodawców.

Wszystkie kursy odbywają się na bazie autorskich materiałów, takich samych niezależnie od miejsca kursu. Dzięki dbałości o jakość kursów oraz uczestnictwie w programie Career Lab, 82% z absolwentów znajduje zatrudnienie w nowym zawodzie w ciągu 3 miesięcy od zakończenia kursu.


Co odróżnia kod napisany przez profesjonalistę od amatora?

Do góry!

Polecane artykuły

25.06.2019

Lead scoring DMsales. Jak działa i jakie daje korzyści dla ...

Głodny wiedzy? Zapraszamy do sklepu z kursami i ebookami

Sprawdzam