SOLID to akronim pięciu zasad, opracowanych przez Roberta Cecila Martina, które przedstawiają sposób pisania elastycznego, łatwego w utrzymaniu, czytelnego kodu w sposób obiektowy. Oczywiście zasady te nie powinny być kajdanami dla programisty, zresztą często trzeba je naginać lub rezygnować z tej lub innej na rzecz konkretnej sytuacji, ale programista powinien robić to świadomie i w miarę możliwości dążyć do ideału opisywanego przez SOLID.
1. Single Responsibility Principle - SRP
Zasada pojedynczej odpowiedzialności lub słowami Martina:
„Klasa powinna mieć tylko jeden powód do zmian.”
Klasa i metoda powinna mieć jeden cel, powinna wykonywać jedną spójną funkcjonalność, i wykonywać ją dobrze. Nie oznacza to, że klasa musi mieć tylko jedną lub dwie metody. Przeciwnie, zasada dotyczy również metod i nawet, jeżeli klasa ją spełnia, ale posiada tylko jedną metodę na kilkadziesiąt lub więcej linii kodu, to nie tylko przestaje być czytelna, ale najpewniej sama ją łamie.
Tą jedną funkcjonalnością, powodem do zmian, tym jednym celem może być np. walidacja danych wejściowych lub tylko ich elementy, jeżeli jest bardziej złożony; może nią być formatowanie danych, wyświetlanie ich, łączenie się z zewnętrznymi serwisami itd.
Jednak czasami trudno jest określić jeden cel, funkcjonalność, szczególnie dla początkującego programisty. Przykładem może być klasa, która zawiera dane użytkownika, sprawdza ich poprawność i zapisuje w bazie danych. Pozornie wszystko może wydawać się w porządku – klasa zajmuje się użytkownikiem, ale w rzeczywistości klasa robić może aż cztery rzeczy: tworzy obiekt, sprawdza poprawność danych, zapisuje je w bazie, z którą musi się połączyć. Zasada SRP mówi nam, że każde z tych zadań powinno znajdować się w osobnej klasie.
2. Open Close Principle – OCP
Zasada otwarte zamknięte lub słowami Martina:
„Elementy oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozbudowę, ale zamknięte dla modyfikacji”.
Dodawanie nowych funkcjonalności (pól klasy, metod z nimi związanych) jest dość oczywiste: rozrost aplikacji, lub zmiana wymagań biznesowych lub pojawienie się nowych tego wymaga, i o ile nie gwałcimy innych zasad SOLID jak SRP to wszystko jest w porządku. Pokusą jest jednak modyfikacja już istniejących funkcjonalności, która może pociągnąć za sobą trudne do przewidzenia konsekwencje w innych miejscach kodu, a jak wiadomo z Prawa Murphyego „Jeżeli coś złego może się stać, to najpewniej się stanie”…
OCP sprowadza się do świadomego używania hermetyzacji, polimorfizmu i dziedziczenia. Ale żeby efektywnie stosować abstrakcję trzeba dostrzec miejsca, które potencjalnie mogą się zmieniać. Przykładem może być sytuacji, w której chcemy zapisać jakieś dane w określonym formacie np. txt i pdf. Metoda, która za pomocą instrukcji warunkowych wywołuje odpowiednie klasy, odpowiadające danemu formatowi jest właśnie takim miejscem. Jeżeli będziemy musieli dodać możliwość zapisu do nowego formatu np. doc, będziemy musieli dodać kolejny warunek, czyli modyfikować już istniejący kod. Rozwiązaniem jest stworzenie interfejsu, który będzie implementowany przez wszystkie klasy przypisane do danego formatu pliku, czyli txt, pdf. Ponieważ będziemy działali na tym interfejsie nie potrzebujemy żadnej instrukcji warunkowej, określającej, do którego formatu zapisać.
3. Liskov Substitution Principle – LSP
Zasada podstawienia Liskov lub słowami Barbary Liskom:
„Jeżeli klasa P (podtyp) jest dzieckiem klasy T (typ), wtedy obiekt typu T może być zamiennie stosowany z obiektem typu P tak, że program będzie się wykonywał poprawnie”.
Kod używający danej klasy powinien poprawnie działać ze wszystkimi jej podklasami. Chodzi o to, aby nie używać dziedziczenia tylko, dlatego, że dana klasa ma coś wspólnego z inną, tylko z powodu, aby nie powielać kodu. Jeżeli w klasie dziedziczącej (P) mamy jakąś metodę, której nie używamy i nadpiszemy ją np. tak, aby wyrzucała wyjątek w razie wywołania, wtedy zasada LSP jest złamana – używając tej klasy, jako typu obiektu klasy nadrzędnej (T) i wywołamy tą metodę, program się wysypie.
Rozwiązaniem może być zastosowanie polimorfizmu; wydzielając jak najwięcej wspólnych cech (pól i metod) klas P i T do nowej klasy tak, że obie klasy P oraz T są postaciami tej nowej klasy, w pełni i poprawnie implementując jej metody.
Zasad LSP jest najtrudniejszą do zrozumienia zasadą SOLID
4. Interface Segregation Principle – ISP
Zasada segregacji interfejsów lub słowami Martina:
„Klient nie powinien być zmuszany do zależności od interfejsów, których nie używa”.
Interfejs nie powinien zmuszać klasy do implementacji metod, których dana klasa nie używa. Zbyt duże interfejsy powinny zostać podzielone na mniejsze. Zasada ta jest w pewien sposób podobna do poprzedniej – LSP, żadna klasa nie powinna zawierać metod, których nie używa tylko, dlatego, że używa innej, a obie są wymagane przez interfejs. Zgodnie z zasadą ISP należy taki interfejs podzielić tak, aby każda klasa implementująca dany interfejs w pełni z niego korzystała.
Jednak dzieląc interfejsy na mniejsze nie należy popadać w skrajność.
5. Dependency Inversion Principle – DIP
Zasada odwróconej zależności lub słowami Martina:
- Wysokopoziomowe moduły nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji.
- Abstrakcja nie powinna zależeć od szczegółów. Szczegóły, czyli konkretne implementacje powinny zależeć od abstrakcji.
Moduły wysokopoziomowe to logika biznesowa, natomiast moduły niskopoziomowe to elementy, klasy do komunikacji z zewnętrznymi serwisami, algorytmy do liczenia itp.
Zastosowanie pierwszej części tej reguły oznacza, że elementy niskiego poziomu możemy z łatwością ponownie używać, ponadto ich ewentualna zmiana nie powinna mieć wielkiego wpływu na moduły wysokiego poziomu, a co za tym idzie na cały program. Zastosowanie drugiego punktu jeszcze dodatkowo uniezależnia nasz program od konkretnej implementacji.
Zawsze tworzenie nowie instancji danej klasy powoduje silne związanie kodu z daną klasą, czyli z jej implementacją. Po przez wstrzykiwanie zależności – przekazywanie obiektów w konstruktorze lub jako parametr metody i jako instancja klasy abstrakcyjnej lub interfejsu – zwiększamy elastyczność kodu i jego odporność podatność na modyfikacje.
Podsumowanie
Jak napisałem na wstępie SOLID mają pomagać programiście, nie go ograniczać. Początkowo jak każda nowa rzecz wydają się skomplikowane, szczególnie LSP oraz DIP, jednak wraz ze zdobywanym doświadczeniem i one stają się jasne tak, jak korzyści z ich stosowania. Korzyści, jakie dają to przede wszystkim elastyczność, łatwość rozbudowy i odporność na problemy wynikłe ze zmian w kodzie (jeżeli już koniecznie trzeba będzie podeptać drugą zasadę). Jednak nie należy się tych zasad fanatycznie trzymać, ale mieć je w pamięci, a porzucać świadomie.