S.O.L.I.D.ne programowanie – część 1, czyli monogamia
S.O.L.I.D.ne programowanie – część 0, czyli wstęp
Witam na pierwszym spotkaniu z zasadami S.O.L.I.D. Temat zajęć Single Responsibility Principle. Ok koniec oficjalnego języka…
Kod jest rodzaju męskiego
Czytałem gdzieś ostatnio, że mężczyzna jest istotą zdolną do wykonywania jednej czynności naraz. Książka była o tym, jak tworzyć udany związek i pisała ją jakaś anarcho-feministka. Ma jednak rację. Mężczyzna nie potrafi robić kilku rzeczy naraz. Wynika to z tego prostego faktu, że chcemy naszą pracę wykonywać dobrze, to się na niej skupiamy.
Podobnie ma się rzecz dobrym kodem.
THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE
Ten cytat jest zazwyczaj podawany jako clou, jeśli chodzi o regułę SRP. Oznacza ona, że zamiany w kodzie mogą być spowodowane tylko przez jedną i tylko jedną przyczynę. Ma to bardzo dobre uzasadnienie w praktycznych rozwiązaniach. Pojedyncza odpowiedzialność pozwala na separację poszczególnych modułów i tym samym utrzymanie kodu staje się łatwiejsze. Sięgając po jakąś klasę, od razu wiemy kto zacz.
Wspominałem o tym, że jeżeli działania kodu nie można opisać w trzech zdaniach, to należy go refaktoryzować. Pojawia się tam też odniesienie do poziomu abstrakcji w ramach którego działamy. No właśnie…
Jeden powód do zmian != jedna czynność
SRP jest prostą regułą, którą wprowadzają osoby, które trochę programują. Jest to samo narzucająca się zasada. Pozwala na unikanie duplikacji kodu, tworzenie modułów Jest to jedna z najbardziej intuicyjnych rzeczy w projektowaniu. Podobnie jak w przypadku singletona można jednak popaść w poważne tarapaty, jeżeli nie rozumie się do końca zasady działania. Mam doskonały przykład jak nie stosować SRP. Zaprogramujmy Modem.
Złe SRP
Modem powinien posiadać następujące funkcje:
- Powinien umożliwiać nawiązanie połączenia.
- Powinien umożliwiać zerwanie połączenia
- Powinien umożliwiać wysłanie wiadomości
- Powinien umożliwiać odbieranie wiadomości
Te cztery funkcjonalności składają się na modem. Warto zauważyć, że nie są one ze sobą sztywno powiązane. Jedyne zależności wynikają z przymusu zachowania kolejności wywołania metod (najpierw połączenie potem komunikacja i na końcu rozłączenie). Tworzą one dwie grupy. Pierwsza odpowiada za połączenie, druga za komunikację. Mamy już zatem dwa interfejsy. Pierwszy będzie zawierał metody connect() i disconnect(), a drugi send() i receive(). Programista będzie używał tych dwóch interfejsów, ręcznie nawiązywał i zrywał połączenie, wysyłał dane i je odbierał. Pakujemy nasz kod do biblioteki. Nazywamy modem i zgarniamy kasiorkę… a tu dupa. Zasadniczo takie podejście jest przykładem złego SRP. Na pierwszy rzut oka odseparowaliśmy odpowiedzialności. Każda z klas ma tylko jeden powód do zmian. Są to odpowiednio zmiana sposobu komunikacji dla klasy Connector i zmiana formatu komunikatów dla klasy Communicator. Jednak nie wykonaliśmy głównego zadania. Nie mamy modemu! Mamy zestaw luźno powiązanych klas, które wrzuciliśmy do jednego wora i nazwaliśmy modem.
Problemem okazuje się zachowanie poziomu abstrakcji przy jednoczesnym stosowaniu SRP. Reguła prowadzi do rozdrobnienia kodu. Jednocześnie zanika nam główny problem. To tak jakbyśmy zamiast na pustynię patrzyli na zbiór ziarenek piachu.
Dobre SRP
Dobre rozwiązanie tego problemu polega na zastosowaniu odpowiedniego poziomu abstrakcji dla modemu. W pierwszym kroku musimy zmodyfikować nasze wymagania:
- Modem umożliwia komunikację z X
Modem ma z definicji jedno zadanie. Umożliwić komunikację. Sposób realizacji tego zadania jest na niższym poziomie. Nadal jedynym powodem zmian jest zmiana sposobu komunikacji, ale nie jest tu już istotne, który element się zmienił. W praktyce modem powinien mieć tylko dwie metody send() i setMessageTarget(). Pierwsza powinna pozwalać na wysłanie wiadomości, a druga na ustawienie miejsca, w które powinny być przekazane wiadomości przychodzące. Klient jest zadowolony, modem ma jedną odpowiedzialność, a w szczegóły nie wnikamy.
Praktyka
Przykład z modemem może wydawać się błędny. W drugim przypadku modem ma wiele odpowiedzialności, ale tylko z punktu widzenia kombinatora. Z punktu widzenia klienta realizuje on dwa zadania w ramach jednej odpowiedzialności – wysyła i pobiera komunikaty. Klienta nie interesują szczegóły. W samym modemie realizowany jest odpowiedni algorytm i jego zmiana jest jedynym powodem zmiany w klasie. Oczywiście poszczególne elementy algorytmu są realizowane przez kolejne podzespoły – klasy i metody, które odpowiadają za coraz węższą działkę.
Zasada jednej odpowiedzialności w praktyce oznacza umiejętne delegowanie bardziej szczegółowych zadań do osobnych klas i metod. Dana klasa realizuje tylko abstrakcyjne zadanie. Nie wnika w szczegóły kolejnych kroków.