Software Development / Architektura

SOLID – dobre praktyki programowania

Marzec 30, 2016 2
Podziel się:

Każdy programista w pewnym momencie swojej kariery zawodowej dojdzie do takiego momentu, w którym zechce poprawić jakość tworzonego przez siebie kodu. Co może wpłynąć na taki stan rzeczy? Z pewnością odpowiedzi na powyższe pytanie jest wiele. Jeśli chodzi o mnie, powodem dla którego zacząłem interesować się dobrymi praktykami programowania było zetknięcie się z niezwykle nieuporządkowanym kodem, nad którym trzeba było siedzieć godzinami żeby można było – po pierwsze go w miarę zrozumieć, a po drugie wprowadzić w nim takie zmiany, aby nie zaburzyły dotychczas działających funkcjonalności.

Dlaczego czytelność kodu jest taka ważna?

Gdy stawiałem swoje pierwsze kroki jako programista powiem szczerze – nie bardzo zwracałem uwagi na to jaki kod wytwarzałem – ważne było dla mnie jedno – zrobić tak, aby działało. Jednakże z czasem, w miarę poprawiania defektów, czy też dodawania nowych funkcjonalności do systemu dało mi się we znaki moje niedbalstwo. Gdy po jakimś czasie wracał do mnie kawałek kodu mojego autorstwa, który kiedyś dla mnie był prosty do zrozumienia, po znaczącym upływie czasu okazywał się praktycznie nie do rozszyfrowania. Wtedy mój bardziej doświadczony kolega dał mi jedną radę „kod powinien być tak pisany, aby można było go czytać bez komentarzy”. Tak, to był dokładnie ten moment, w którym postanowiłem zgłębić tajniki dobrych praktyk programowania oraz wzorców projektowych. Postanowiłem wtedy zrobić małe rozeznanie i podpytać bardziej doświadczonych kolegów o tytuły książek, które pomogły by mi poprawić jakość pisanego przeze mnie kodu.

Czym jest SOLID?

W miarę zgłębiania tematu poznałem 5 podstawowych reguł, które odmieniły moje podejście do wytwarzania kodu, oto one:

  • Zasada pojedynczej odpowiedzialności (ang. Single-Responsibility Principle – SRP),
  • Zasada otwarte – zamknięte (ang. Open/Closed Principle – OCP),
  • Zasada podstawiania Liskov (ang. Liskov Substitution Principle – LSP),
  • Zasada segregacji interfejsów (ang. Interface Segregation Principle – ISP),
  • Zasada odwracania zależności (ang. Dependency Inversion Principle – DIP).

Łatwo można zauważyć, iż po połączeniu pierwszych liter skrótów w j. angielskim wyżej wymienionych reguł otrzymamy SOLID. SOLID jest zatem skrótem opisującym podstawowe założenia programowania obiektowego.

Zasada pojedynczej odpowiedzialności

Zasadę pojedynczej odpowiedzialności można w sumie opisać jednym prostym zdaniem, a brzmi ono następująco: „Żadna klasa nie może być modyfikowana z więcej niż jednego powodu”.

  Z mojego doświadczenia wynika, iż jeśli jakaś klasa jest odpowiedzialna za więcej niż jeden obszar naszego projektu jest to bardzo niekorzystne. Może się zdarzyć bowiem, że modyfikując jeden obszar mimowolnie wpłyniemy na zupełnie inny, który nie jest w żaden sposób związany z pierwszym. Dodatkowo, każdy taki związek wpływa negatywnie na cały projekt poprzez zmniejszenie jego elastyczności, a co za tym idzie – prowadzi do nieoczekiwanych rezultatów wprowadzanych przez nas zmian.

Zasada pojedynczej odpowiedzialności w teorii jest jedną z najprostszych ze wszystkich wymienionych przeze mnie reguł, jednakże, jeśli chodzi o praktykę jest najtrudniejsza. Muszę przyznać, że sporo czasu minęło, zanim w 100% nauczyłem się ją wykorzystywać.

Zasada otwarte – zamknięte

Zasada otwarte – zamknięte brzmi następująco: „Składniki oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozbudowę, ale zamknięte dla modyfikacji”.

Rozbijając powyższą definicję na mniejsze części, można wydobyć dwa kluczowe atrybuty, którymi powinny się charakteryzować tworzone przez nas moduły, są nimi: „otwarte na rozbudowę” oraz „zamknięte dla modyfikacji”.

Co należy rozumieć przez stwierdzenie, że moduł musi być „otwarty na rozbudowę”? Mianowicie to, że musi istnieć stosunkowo prosty sposób rozbudowy zachowań takiego modułu.

„Zamknięte dla modyfikacji” oznacza z kolei, że rozbudowa modułu nie może być przeprowadzona w sposób, który spowoduje zmianę istniejącego kodu źródłowego.

Jestem pewien, że każdy z nas podczas swojej pracy miał do czynienia ze zmieniającymi się wymaganiami aplikacji. Zgodność naszego projektu z tą zasadą jest jednym z warunków, które zagwarantują nam takie korzyści jak: elastyczność, możliwość wielokrotnego wykorzystywania istniejącego kodu, czy łatwość konserwacji naszego projektu.

Zasada podstawiania Liskov

Można powiedzieć, że Zasada podstawiania Liskov jest jednym z warunków zasady otwarte – zamknięte. Jej definicja wygląda następująco: „Musi istnieć możliwość zastępowania typów bazowych ich podtypami”.

Dlaczego uważam, że zasada podstawiania Liskov jest jednym z warunków zasady otwarte – zamknięte? Między innymi dlatego, że możliwość zastępowania podtypów umożliwia rozbudowę modułów (klas, typów bazowych) bez konieczności ich bezpośredniego modyfikowania.

W celu zobrazowania działania zasady podstawiania Liskov, posłużę się prostym przykładem. Załóżmy, że posiadamy dwie klasy, w których znajdujemy wiele wspólnych składowych. Wspólnymi składowymi mogą być: zbiór właściwości, metod itd. Co w takiej sytuacji należałoby zrobić? Zgodnie z zasadą podstawiania Liskov, wypadałoby wyodrębnić wspólne elementy obu klas do jednej klasy – najlepiej abstrakcyjnej.

Wyodrębnianie składowych jest niezwykle silną bronią każdego programisty. Jeżeli bowiem istnieje możliwość wyodrębnienia identycznych składowych z dwóch lub większej ilości klas, to jest wielce prawdopodobne, że przyjdzie taki moment, w którym inne klasy będą do tych składowych się odwoływały.

 

Zasada segregacji interfejsów

Zasada segregacji interfejsów ma za zadanie przede wszystkich wyeliminowanie nieporęcznych, niepotrzebnie rozbudowanych interfejsów. Do tej grupy można zaliczyć nadmiernie rozbudowane i niespójne interfejsy klas. Każdy taki interfejs zgodnie z tą zasadą powinien zostać podzielony na mniejsze grupy metod.

Przeanalizujmy teraz mały przykład. Załóżmy, że posiadamy pewien interfejs oraz dwie klasy, które go implementują. Klasa A implementuje nasz przykładowy interfejs w całości – wszystkie funkcje, natomiast klasa B posiada pełną implementację tylko części z nich, pozostałe, które są niepotrzebne pozostają puste lub zwracają domyśle wartości. Jeśli mamy do czynienia z taką sytuacją, powinniśmy rozważyć rozdzielenie interfejsu na mniejsze tak, aby każdy z nich deklarował tylko te funkcje, które rzeczywiście są wywoływane przez danego klienta lub grupę klientów (klasa/grupy klas implementujące dany interfejs).

Opisany powyżej model eliminuje zależność obiektów klienckich od metod, których nie wywołują. Umożliwia to zapewnianie wzajemnej niezależności samych klientów.

 

Zasada odwracania zależności

Zasada odwracania zależności składa się z dwóch następujących części:

  1. Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obie grupy modułów powinny zależeć od abstrakcji.
  2. Abstrakcje nie powinny zależeć od szczegółowych rozwiązań. To szczegółowe rozwiązania powinny zależeć od abstrakcji.

Wydaje mi się, że powyższa definicja zasady odwracania zależności powinna być łatwa do zrozumienia.

Zastanówmy się jednak przez chwile, jakie mogą być skutki zależności modułów wysokopoziomowych od niskopoziomowych, czyli co by się działo, gdybyśmy nie postępowali według pierwszej części definicji zasady odwracania zależności. Moduły wysokiego poziomu z natury rzeczy zawierają ważne decyzje strategiczne i modele biznesowe danej aplikacji. Myślę, że wszyscy się ze mną zgodzą, iż te właśnie moduły w największym stopniu odpowiadają za funkcjonowanie aplikacji. Gdyby okazało się, iż zależą one od modułów niskiego poziomu, to zmiany elementów niskiego poziomu mogłyby mieć wpływ na funkcjonowanie modułów wysokopoziomowych, a co za tym idzie wymuszałyby zmiany na wyższych poziomach. Dodatkowo, gdy moduły wysokopoziomowe zależą od niskopoziomowych, ponowne ich wykorzystanie staje się niezwykle trudne. Jeśli jednak odwrócimy tę zależność w drugą stronę, to bardzo łatwo będzie można wielokrotnie je wykorzystywać.

Jeśli chodzi o zależność od abstrakcji, można to zamknąć w jednej prostej formule: pisany przez nas kod nie powinien być uzależniony od konkretnej klasy, zależności takie powinny kończyć się na klasach abstrakcyjnych bądź interfejsach. Zasadę zależności od abstrakcji (ang. depend on abstractions) można również streścić w trzech prostych punktach:

  • Żadna zmienna nie powinna zawierać referencji do konkretnej klasy.
  • Żadna klasa nie powinna dziedziczyć po konkretnej klasie.
  • Żadna metoda nie powinna przykrywać metody zaimplementowanej w którejkolwiek z klas bazowych.

Podstawowymi zaletami zasady odwracania zależności jest to, że właściwe jej stosowanie jest kluczowe, jeśli chcemy tworzyć frameworki wielokrotnego użytku. Ma ona również duży wpływ na odporność kodu źródłowego na przyszłe zmiany, ponieważ zgodnie z tą zasadą abstrakcje, a także szczegółowe mechanizmy są od siebie odizolowane, co z kolei wpływa na to, że tworzony kod jest dużo prostszy w konserwacji.

Podsumowanie

W artykule tym przedstawiłem w wielkim skrócie zasady, które kryją się pod skróconą nazwą SOLID. Mam świadomość, że tak naprawdę każda z wyżej wymienionych reguł zasługuje na odrębny artykuł na jej temat. Jestem pewien, że każdemu programiście dużo przyjemniej pracowałoby się w projekcie, w którym zasady SOLID były stosowane. Ułatwiałoby to bowiem odnalezienie się w projekcie, co z pewnością wpłynęło by na zwiększenie wydajności zespołów projektowych. Ze swojej strony mogę jedynie zachęcić do zgłębiania tego typu wiedzy, traktujmy pisany przez nas kod jak swoją wizytówkę, a poskutkuje to tym, że wszyscy, którzy będą mieli z nim do czynienia będą nam wdzięczni – po pierwsze – za przejrzystość, jak i również łatwość rozwijania czy też utrzymywania aplikacji.

4.6 / 5
Norbert Pietroń
Autor: Norbert Pietroń
Zawodowo .NET Developer odpowiedzialny za tworzenie, rozwój oraz wsparcie aplikacji. Prywatnie fan koszykówki, ligi zawodowej NBA oraz Arsenalu Londyn.

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

komentarze(2)

mszarlinski@gmail.com'
mszarlinski
1 kwietnia 2016 Odpowiedz

Gratuluję wpisu na blogu. Mam jednak uwagę do następującego stwierdzenia dotyczącego LSP:
"Co w takiej sytuacji należałoby zrobić? Zgodnie z zasadą podstawiania Liskov, wypadałoby wyodrębnić wspólne elementy obu klas do jednej klasy – najlepiej abstrakcyjnej.
Wyodrębnianie składowych jest niezwykle silną bronią każdego programisty."

Według mnie jest ono zbyt ogólne i może być opacznie zrozumiane.
1. LSP nie nakazuje wyodrębniania nadklas. Zasada opisuje jedynie kontrakt między typem a jego podtypami. Mówiąc jeszcze inaczej, jaki warunek musi spełniać dany typ T1, aby być podtypem dla T2.
2. Tworzenie nadklas dlatego, że kilka klas posiada podobne atrybuty jest niewystarczającym powodem i prowadzi do ustalenia trwałej relacji między typami na etapie kompilacji. Dobrą praktyką, która otwiera wrota do świata wzorców projektowych, jest przedkładanie kompozycji ponad dziedziczenie (polecam lekturę "Head First Design Patterns"). Dziedziczyć powinniśmy zachowania, a nie stan.

kisielewskitomaszek@go2.pl'
architekt Łódź
28 listopada 2018 Odpowiedz

Intersujący wpis i błyskawicznie się go czyta, zwłaszcza, że jest napisany bardzo zrozumiałym, lekkim językiem.

http://apa.info.pl

Zostaw komentarz