Blog
W trakcie ostatniego spotkania technicznego zespołu, który współtworzę, rozmawialiśmy nt. aktualizacji checklisty, zgodnie z którą przeprowadzamy review kodu. Przez dłuższy czas dyskutowaliśmy o wprowadzeniu w niej nowego zapisu – kod musi spełniać zasady SOLID. Długo lobbowałem w zespole za tym wymogiem. Finalnie jednak nie zdecydowaliśmy się go wprowadzić, co skwitowałem, słowami:
“Intuicyjnie czuję, że rezygnujemy z czegoś bardzo istotnego”
Zastanawiałem się później nad tymi słowami. Bardziej pasują do wróżbity Macieja niż doświadczonego programisty :). Znam każdy z pryncypiów składowych SOLID-a, czemu więc do głosu doszła moja intuicja? Doszedłem do wniosku, że nigdy nie zastanawiałem się nad SOLID-em jako całością. Czemu te pryncypia zostały zestawione razem? Co tracimy stosując je rozłącznie?
Czynniki pierwsze
Aby rozważyć, co daje SOLID jako całość, bardzo zwięźle przypomnę sens jego składowych:
Single responsibility – Klasa ma mieć tylko jeden powód do zmiany;
Open/close – Klasa ma być otwarta na rozszerzanie, a zamknięta na zmiany;
Liskov substitution – Klasa, która deklaruje, że implementuje daną abstrakcję, ma to robić poprawnie, tak aby kod polegający na abstrakcji, nie musiał być zależny od konkretnych jej implementacji, ale pozwalał na podstawienie różnych implementacji tej abstrakcji.
Interface segregation – Klasa nie powinna być zależna od metod, których nie używa.
Dependency inversion – Klasa wyższego poziomu nie powinna być zależna od klasy niższego poziomu, obie powinny zależeć od abstrakcji.
Pełny SOLID
Załóżmy, że w naszej aplikacji istnieje klasa A i abstrakcja B.
class A { public function methodA(B $b) { // code } } interface B { public function methodB(); }
- Klasa “A” jest zależna od abstrakcji “B” (DI), dzięki czemu możemy podstawiać dowolne implementacje tej abstrakcji, rozszerzając funkcjonalność klasy “A” bez potrzeby jej modyfikacji (O/C).
- Powyższe jest prawdą, o ile klasy deklarujące implementację abstrakcji “B” robią to poprawnie, co pozwoli stosować je zamiennie (LS). W przeciwnym razie będą wymagane zmiany w klasie “A”.
- Dodatkowo abstrakcja “B” jest dopasowana do potrzeb klasy “A” tj nie zawiera zbędnych zależności (metod), dzięki czemu łatwiej tworzyć/adaptować kolejne konkretne klasy aby implementowały “B” (IS).
- Poza tym klasa “A” i wszystkie implementacje “B” są małe, wymagają zmian jedynie wtedy, gdy ma się zmienić ich podstawowa funkcjonalność/odpowiedzialność (SR).
Jak to się przekłada na codzienną pracę z kodem?
W idealnym przypadku raz napisana klasa nie będzie musiała być nigdy zmieniona (jedynie wtedy, gdy jej pojedyncza odpowiedzialność musi ulec zmianie). Aplikacja jest rozwijana przyrostowo – istniejącej bazy kodu nie modyfikujemy tylko rozszerzamy poprzez nowe implementacje istniejących abstrakcji lub dodajemy nowe abstrakcje i ich implementacje. Dzięki temu nie wystąpi regresja w istniejących jednostkach kodu. SOLID chroni typy w naszej aplikacji.
Prawie robi wielką różnicę?
Jak widać pryncypia się przenikają, walidują i mają jasny cel. Czy można stosować je wybiórczo? Można, zmniejszy to ryzyko potrzeby modyfikacji istniejacych klas, ale ryzyko będzie większe niż przy przestrzeganiu pełnego SOLID.
Kombinacje jakie dostrzegam:
SOLID – klasa jest mała, ale ma silne zależności. O ile ryzyko zmiany w odpowiedzialności jednej klasy jest niskie, to w przypadku, gdy jest ona zależna od kilku klas (równie małych), ryzyko rośnie proporcjonalnie do liczby zależności.
SOLID – klasa jest otwarta na rozszerzenie i zależna od abstrakcji. Niestety zbyt duże zależności (rozmiar abstrakcji) utrudniają rozszerzanie klasy bez złamania LS. Dodatkowo, jako że klasa jest duża, to jest większe ryzyko, że będzie wymagała zmiany.
Tyle moich rozważań, z chęcią poznam Twoją opinię…