Ostatnio poświęciłem trochę czasu na bliższe przyjrzenie się zasadom z zestawu SOLID, więc postanowiłem podzielić się swoimi spostrzeżeniami i kilkoma krótkimi przykładami w Swift 😆.

SOLID jest skrótem od Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, oraz Dependency Inversion Principle. Jest to zestaw najlepszych praktyk jakie powinno stosować się podczas tworzenia aplikacji w ujęciu ogólnym, nie tylko na systemy mobilne. Termin ten został rozpowszechniony przez Roberta C. Martina znanego również jako Uncle Bob. Rozumienie i odpowiednie wykorzystanie wszystkich zasad SOLID wymaga czasu i doświadczenia, ale z całą pewnością się opłaci. Można je wprowadzać stopniowo, w miarę jak nasze zrozumienie będzie ewoluowało. Implementacja nawet jednej z nich znacznie poprawi jakość naszego kodu. Jak to bywa z zasadami, ciężko jest zawsze stosować je w 100 % 😎. Przyznam szczerze, że ja sam ciągle muszę się pilnować 😏.

 

 

The Single Responsibility Principle (SRP)

 

A class should have one and only one reason to change, meaning that a class should only have one job.

 

Chyba najważniejsza zasada ze wszystkich. Zakłada, że każdy moduł powinien mieć tylko jedną odpowiedzialność (w sensie robić tylko jedną rzecz) i tylko jeden powód do zmiany. Najlepszym podejściem przy stosowaniu tej reguły jest tworzenie na samy początku podstawowego obiektu (przykładowo klasy / struktury), który będzie wykonywał tylko jedno określone zadanie – takie absolutne minimum. Jeżeli w trakcie dalszych prac nad obiektem pojawi się dodatkowa odpowiedzialność to należy przenieść ją od razu do osobnego obiektu.

Przykład z życia codziennego – tworzymy klasę do pobierania danych ze zdalnego API i na początku klasa ta odpowiada tylko za tę funkcjonalność:

 

 

W pewnym momencie okazuje się, że dane pobrane z serwera będą zapisywane do lokalnej bazy danych, żeby można było łatwiej je ponownie wykorzystać. Więc dodajemy właściwy kod do naszego obiektu:

 

 

Właśnie w tym momencie łamana jest zasada pojedynczej odpowiedzialności (SRP). Klas WebService powinna odpowiadać jedynie za pobieranie danych z serwera. Ich lokalnym zapisem powinien zająć się osobny obiekt. Można to rozwiązać w bardzo prosty sposób:

 

 

 

SRP pomaga nam opierać naszą aplikację na mniejszych, niezależnych i wyspecjalizowanych modułach. Powtarzam to gdzie tylko się da, ale właśnie takie podejście sprawi, że pisanie testów stanie się łatwiejsze lub nawet w ogóle możliwe. Czytelność naszego kodu również znacznie wzrośnie.

 

The Open/Closed Principle (OCP)

 

Objects or entities should be open for extension, but closed for modification.

 

Zasada ta zakłada, że nasze moduły powinny być otwarte na rozszerzenie ich funkcjonalności, ale modyfikacja istniejącego kodu powinna być zablokowana. Oznacza to, że powinniśmy mieć możliwość zmiany zachowania naszego modułu bez zmiany kodu źródłowego. Wprowadzanie zmian w modułach, od których może zależeć działanie wielu innych komponentów może okazać się fatalne w skutkach, zwłaszcza jeżeli nie ma odpowiednich testów, które mogą to wyłapać. Znaczące zmiany wprowadzane w już istniejącym kodzie mogą spowodować, że trzeba będzie przepisywać na nowo wiele linijek kodu w różnych miejscach i modułach.

Ta zasada sprawdza się oczywiście głównie w dużych projektach, w których znajduje się cała masa rozbudowanych komponentów. Jeżeli macie własny, mały projekt to nic wielkiego się nie stanie jak zmienicie logikę w jakiejś klasie bazowej, a nawet jeśli, to z pewnością szybko błąd wyłapiecie.

Rozszerzyć możliwości danego komponentu możemy na przykład poprzez dziedziczenie, polimorfizm lub interfejsy. W Swift zadanie jest o tyle łatwe, że wystarczy skorzystać z Extensions.

OCP jest koncepcją, którą trudno dobrze zrozumieć bez odpowiedniej praktyki. Przynajmniej ja mam takie odczucia.

Przejdźmy teraz do kodu. Za przykład posłuży nam wcześniejszy obiekt WebService:

 

 

Funkcja getEmployeesData() pobiera dane o pracownikach na podstawie departamentu. Teraz załóżmy, że w firmie pojawia się nowy dział – Project Management. Musimy dodać odpowiednie pole dla enumeracji, a dodatkowo w funkcji getEmployeesData() musimy dodać kolejny przypadek do instrukcji switch. To jest bardzo mały przykład i być może dokładnie tego nie widać, ale w przypadku dużych projektów takie mieszanie w komponencie, który został już wcześniej przetestowany może doprowadzić do niepotrzebnych kłopotów i sporej ilości dodatkowej pracy.

Zasada OCP stara się chronić nas przed takimi właśnie problemami. Dużo lepszym rozwiązaniem będzie rozbicie funkcji z powyższego przykładu na mniejsze, niezależne elementy:

 

 

Załóżmy teraz, że bardzo nie chcemy już mieszać w naszym obiekcie WebService (bo jest już bardzo stary i sprawdzony z każdej strony😜), ale będziemy chcieli dodać nowy dział Project Management. Możemy skorzystać z prostego dziedziczenia lub rozszerzenia w przypadku języka Swift lub Kotlin:

 

 

Tak jak w przypadku każdej zasady SOLID nie ma tutaj jednego, słusznego rozwiązania. Musimy tylko pamiętać o tym żeby tworzyć jak najmniejsze obiekty, składające się z wielu małych funkcji zamiast kilku nadmiernie rozbudowanych.

 

The Liskov Substitution Principle (LSP)

 

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects of type S where S is a subtype of T.

 

Zasada ta zakłada, że powinniśmy mieć możliwość wstawienia w miejsce obiektu bazowego dowolnego obiektu, który jest jego typem podrzędnym. Taka podmiana nie powinna wpływać na poprawność wykonania naszego programu.

Prosty przykład – załóżmy, że mamy protokół o nazwie Example, który określa pewne metody do zaimplementowania:

 

 

Teraz załóżmy, że mamy dwa obiekty, które implementują ten protokół – SmallExample i BigExample:

 

 

Zasada LSP zakłada, że w każdym miejscu, gdzie wykorzystywany będzie obiekt typu Example możemy wstawić obiekt SamllExample lub BigExaample i bez względu na nasz  wybór program będzie w dalszym ciągu wykonywał się zgodnie z założeniami. Jest to pewien rodzaj kontraktu. Nasze obiekty bazowe gwarantują, że ich obiekty pochodne będę w stanie wykonać określone zadania. Tak to będzie wyglądało na przykładzie:

 

 

Obiekt SomeObject może teraz wykorzystać dowolnie SmallExample lub BigExample, a program się skompiluje i wykona poprawnie:

 

 

 

 

 

The Interface Segregation Principle (ISP)

 

A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

 

Zasada ta mówi, że lepiej mieć kilka bardziej szczegółowych interfejsów niż jeden ogólny. Zakłada ona również, że obiekt wykorzystujący dany interfejs nie powinien być zależny od metod, których nie będzie wykorzystywał. Wyobraźcie sobie sytuację, w której macie jeden ogólny protokół i tworzycie obiekty z jego implementacją:

 

 

Zmuszanie obiektów do implementacji metod, których nie będą wykorzystywały nie jest dobrym rozwiązaniem, ponieważ w ten sposób będziemy dodawać do naszej aplikacji zbędny kod. Znacznie korzystniej będzie rozbić powyższy protokół na kilka mniejszych i przypisywać je tylko w razie faktycznej potrzeby. Niektórzy programiści twierdzą, że takie podejście powoduje tworzenie zbyt dużej ilości protokołów (interfejsów), które trzeba następnie utrzymywać. Rozumiem ich punkt widzenia, jednak ja w dalszym ciągu wolę stawiać na mniejsze obiekty 😉.

 

The Dependency Inversion Principle (DIP)

 

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.

 

To chyba moja ulubiona zasada 🤓. Mówi ona, że nasz kod powinien opierać się na abstrakcjach, a nie na konkretnych implementacjach.  Najlepszym przykładem zastosowania tej reguły jest Dependency Injection (DI). DIP pozwala na tworzenie niezależnych od siebie modułów, które komunikują się tylko za pomocą abstrakcji (zwykle są to interfejsy). Dla przykładu użyjemy protokołu DataWebService, który będzie wykorzystywany przez obiekt typu kontroler, stanowiący pomost pomiędzy poszczególnymi komponentami w naszej aplikacji (zgodnie z założeniami wzorca MVC):

 

 

A tak będzie wyglądał kontroler w bardzo uproszczonej wersji:

 

 

W tym miejscu możecie sprawdzić bardziej złożony przykład i poczytać trochę o wzorcu MVC.

 

Teraz załóżmy, że chcemy napisać testy dla naszego kontrolera. Korzystanie z konkretnej implementacji CustomWebService będzie nam utrudniało zadanie, ponieważ chcemy tylko symulować połączenie z serwerem. Dzięki temu, że obiekt ExampleController współpracę z DataWebService opiera na interfejsie, możliwe jest podanie mu dowolnego obiektu, który spełnia wymogi tego protokołu. Czyli z łatwością możemy wstawić obiekt, który będzie jedynie udawał, że łączy się z serwerem, a w rzeczywistości będzie zwracał nam za każdym razem wybrane przez nas wartości. Na ich podstawie będziemy mogli zweryfikować poprawność działania naszego kontrolera. To jest właśnie główna zaleta zasady DIP. Opierając komunikację między modułami na abstrakcjach jesteśmy w stanie testować je w izolacji.

Jeżeli chcecie sprawdzić ten przypadek na bardziej rozbudowanym przykładzie to możecie odwiedzić link umieszczony powyżej 😊.

 

Słowo na drogę

 

Zrozumienie wszystkich zasad SOLID i ich poprawna implementacja nie należą do rzeczy najłatwiejszych. Ja sam mam ciągle z tym sporo trudności, chociaż temat znam już od dłuższego czasu. Z całą pewnością warto jednak włożyć w naukę SOLID’a trochę wysiłku. Za jakiś czas sami sobie podziękujecie, a podejrzewam, że koledzy / koleżanki z Waszego zespołu również będą Wam bardzo wdzięczni 😎. Podsumowując w kliku słowach – dzięki stosowaniu zasad SOLID Wasz kod stanie się bardziej modularny, czytelny i łatwiejszy w testowaniu. Do usłyszenia 🧐.

 


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *