Wybierz region
pl
  • PL
  • EN
Wydrukuj

Programowanie S.O.L.I.D w praktyce

W dobie otaczającej nas rewolucji cyfrowej technologia oprogramowania rozwija się bardzo dynamicznie, udostępniając co chwilę nowe standardy komunikacji i nowe narzędzia do budowy ekosystemów IT takie jak biblioteki programistyczne czy całe frameworki. Giganci z branży zmienili podejście do wersjonowania swoich produktów wypuszczając kwartalnie ich kolejne wersje. Wcześniej było to wykonywane z częstotliwością raz na kilkanaście lub więcej miesięcy.

Jako developerzy skupiamy się na poznawaniu nowych technologii implementujących często te same funkcjonalności biznesowe wierząc w to, że modna biblioteka czy trendy framework rozwiąże za nas trudne problemy technologiczne albo uchroni przed złożonością domeny. Tak nie jest, a skutki takiego myślenia możemy czasami zaobserwować w systemach, które tworzymy.

Na szczęście istnieją aspekty developmentu, które nie straciły na aktualności od momentu ich opublikowania. Są to tzw. zasady projektowania oraz wzorce projektowe. Występują w wielu dziedzinach życia oprócz IT (np. w budownictwie) i są dużej mierze niezależne od technologii.

W niniejszym artykule przedstawię podejście, do tworzenia programów oparte na popularnych zasadach projektowania znanych pod dobrze brzmiącą nazwą SOLID.

Zasady SOLID stanowią zbiór wytycznych, które pomagają nam uniknąć podstawowych błędów podczas tworzenia aplikacji komputerowych. Nazwa SOLID to akronim od pierwszych liter najważniejszych zasad. Zostały one opracowane przez Roberta C. Martina, który opublikował je w manifeście „Design Principles and Design Patterns” oraz w swojej książce pt. „Agile Software Development: Principles, Patterns and Practice”.

Według Roberta Martina istnieją 3 najważniejsze cechy złego projektu, których należy unikać:

  • Sztywność – trudno to zmienić, ponieważ każda zmiana wpływa na zbyt wiele innych części systemu,
  • Kruchość – po wprowadzeniu zmiany nieoczekiwane części systemu ulegają awarii,
  • Nieprzenośność – ponowne użycie w innej aplikacji jest trudne, ponieważ nie można jej rozdzielić od aktualnych zależności.

Wyżej wymienione wady oprogramowania można zminimalizować, jeśli sposób rozwijania tego oprogramowania będzie zgodny z niżej wymienionymi pięcioma zasadami. Reguły te zaprezentuję i opiszę poniżej stosując przykłady w popularnym języku obiektowym.

Single Responsibility Principle

A class should have only one reason to change.

Klasa powinna mieć tylko jeden powód do zmiany. W tym kontekście odpowiedzialność uważa się za jeden z powodów zmiany. Zasada ta mówi, że jeśli mamy 2 powody, by zmienić klasę, musimy podzielić funkcjonalność na dwie klasy. Każda klasa poradzi sobie tylko z jedną odpowiedzialnością, a w przyszłości, jeśli będziemy musieli dokonać jednej zmiany, zrobimy to w klasie, która ją obsługuje. Kiedy musimy wprowadzić zmiany w klasie, która ma więcej obowiązków, zmiana może wpłynąć na kod, który nie powinien być modyfikowany.

Czas na przykład złamania zasady SRP:

Kod programu po korekcie:

W kodzie po zmianie zmodyfikowano sygnaturę metody setContent() z parametrem IContent po to, aby obiekt Email nie musiał być modyfikowany za każdym razem, gdy wymaganie biznesowe wprowadzi nowy format do korespondencji np. HTML, etc. Drugi powód do zmiany klasy Email to protokół komunikacji np. POP3, IMAP, etc.

 

Open Close Principle

Software entities like classes, modules and functions should be open for extension but closed for modifications.

Elementy oprogramowania, takie jak klasy, moduły i funkcje, powinny być otwarte na rozszerzenia, ale zamknięte dla modyfikacji. Zasada ta jest stosunkowo intuicyjna, bo polega na tym by tak zaprojektować rozwiązanie, aby implementacja nowych funkcjonalności nie zmieniała istniejącego kodu a jedynie go rozszerzała. Najczęstsze sposoby na zastosowanie tej zasady to tzw. polimorfizm, wzorzec strategii i odwrócenie zależności.

Przykład:

W poniższym przykładzie nie spełniającym zasady OCP widać, że klasa Drawer musi zostać zmodyfikowana za każdym razem gdy pojawi się wymaganie rysowania nowego rodzaju kształtu.

Po zastosowaniu zasady OCP klasa Drawer nie jest zmieniana, gdy dochodzi nowy kształt:

Liskov's Substitution Principle

Derived types must be completely substitutable for their base types.

Typy pochodne muszą całkowicie zastępować typy podstawowe. Zasada ta jest rozszerzeniem zasady Open Close Principle pod względem zachowania, co oznacza, że nowe klasy pochodne rozszerzają klasy podstawowe bez zmiany ich zachowania. Innymi słowy klasy pochodne powinny móc zastąpić klasy bazowe bez żadnych zmian w kodzie. Zasadę tę Robert Martin zaczerpną od Barbary Liskov (profesor MIT). Została ona opublikowana w 1987r.

Przykład:

Opisuje zastąpienie klasy bazowej Rectangle jej rozszerzeniem w postaci klasy Square.

Powyższy przykład pokazuje, że zasada zastępowalności LSP nie została utrzymana, ponieważ wynik funkcji getArea() nie działa tak samo, gdy zastąpimy klasę pochodną klasą bazową.  

 

Interface Segregation Principle

Clients should not be forced to depend upon interfaces that they don't use.

Klienci nie powinni być zmuszani do polegania na interfejsach, których nie używają. Ta zasada uczy nas dbać o to, jak piszemy nasze interfejsy. Pisząc je, powinniśmy zadbać o to, aby dodać tylko metody, które powinny tam być. W przeciwnym wypadku, klasy implementujące interfejs również będą musiały zaimplementować te metody. Na przykład, jeśli utworzymy interfejs o nazwie Worker i dodamy metodę przerwy na posiłek, to wszyscy pracownicy będą musieli ją zaimplementować.

A co, jeśli pracownik jest robotem?

Przykład:

Implementacja niespełniająca zasady ISP. Obiekt Manager otrzymuje kompetencje i teoretyczną możliwość wywołania metody eat() obiektu Worker. Błąd!

Przykład kodu po korekcie spełniającego ISP:

Obiekt Manager, który ma zależność do obiektu pracownika IWorkable otrzymuje dostęp tylko do tych funkcji, którymi jest zainteresowana.

 

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji. Zasada odwrócenia zależności określa, że powinniśmy oddzielać moduły wysokiego poziomu od modułów niskiego poziomu, wprowadzając warstwę abstrakcji między klasami wysokiego poziomu a klasami warstwy niższej. W klasyczny sposób, gdy moduł oprogramowania (klasa, komponent) potrzebuje jakiegoś innego modułu, inicjuje się i zawiera bezpośrednie odniesienie do niego. Spowoduje to ścisłe połączenie tych dwóch elementów. Stosując odwrócenie zależności poprzez warstwę abstrakcji, moduły mogą być łatwo zamieniane na inne.

Jest to w mojej opinii najważniejsza zasada, dotycząca nie tyle projektu systemu co jego architektury.

 

Przykład kodu łamiącego zasadę DIP:

Widać, że obiekt Manager jest bezpośrednio zależny od obiektu Worker, co powoduje, że te dwa elementy są ze sobą trwale powiązane i zmiana Worker na alternatywny nie może zostać wykonana bez modyfikacji klasy Manager.

Kod spełniający zasadę DIP

Po korekcie obiekt Manager został pozbawiony sztywnej zależności do obiektu Worker, co daje nam możliwość podmiany implementacji interfejsu IWorker w czasie rzeczywistym bez modyfikowania warstwy wyższego rzędu.

Zasada odwrócenia zależności wspiera również zasadę Open Close Principle, ponieważ wersja programu po korekcie może być rozszerzana bez konieczności modyfikowania istniejących klas i modułów.

Bardzo dobrym miejscem do zastosowania zasady DIP jest styk warstwy aplikacji z warstwą dostępu do danych (DAO). Warstwa aplikacji powinna być zależna jedynie od zbioru interfejsów (warstwa abstrakcji), których używa w sposób ślepy „nie wiedząc” jaka technologia DAO jest aktualnie w użyciu. Taka struktura aplikacji umożliwia wymianę technologii DAO oraz DBMS’u w sposób niewidoczny dla reszty architektury systemu.

 

Podsumowanie i wnioski:

Jak już pewnie zauważyliście, nie wszystkie wymienione zasady mają jasno zaznaczone granice. W rezultacie czego, nie możemy stwierdzić z całą pewnością, że jakaś implementacja jest w pełni zgodna bądź w całości niezgodna z danym pryncypium. Jest to pewne kontinuum, a developer powinien dążyć, aby zgodność z zasadami SOLID była możliwie jak najwyższa. Dotyczy to chociażby zasady Single Responsibility Principle, gdzie liczba powodów do zmodyfikowania klasy może być kwestią subiektywną. Ze doświadczenia wiem, że ćwiczenie jest najlepszą drogą do opanowania tej sztuki. Jednak najważniejszą zasadą, która powinna być utrzymana podczas budowy każdego systemu to wg mnie zasada zdrowego rozsądku.

 

Źródła:

https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf - Robert C. Martin

https://sourcemaking.com

https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples

https://medium.com/better-programming/solid-principles-simple-and-easy-explanation-f57d86c47a7f

https://www.baeldung.com/solid-principles


Krzysztof Masłyk

Pracuję w Pionie Banków Komercyjnych, gdzie projektuję rozwiązania dla systemu ARS. Wymyślanie, tworzenie i rozwiązywanie problemów daje mi dużą frajdę, dlatego w większości przypadków pracę traktuję jak hobby:) W czasie wolnym rozwijam umiejętności z obszaru machine learning i pythona. Moje zainteresowania pozazawodowe to muzyka i sport.


Wydrukuj