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.

8 myśli na temat “S.O.L.I.D.ne programowanie – część 1, czyli monogamia

  1. Bardzo dobry przykład z tym modemem, bardzo dobry. Niestety patrząc na API roznych bibliotek (nawet tych, ktore wychodzą ze stajni Suna czy Apache) czesto brakuje tego „modemu” z jednym wspaniałym czerwonym i nabrzmiałym guzikiem – „załatw sprawę”. Zamiast tego mamy tak jak napisałeś wór jakiś klamotów, ktore i tak musimy pakować w nasze własne klasy po to aby stworzyć z nich działające maszyny. Oczywiście fajnie byłoby gdyby w razie potrzeby można było pogrzebać w modemie i np ustawić mu strategie robienia penych szczegółów niższego poziomu.

  2. Fajny tekst, i dobry przykład, niestety z praktycznego punktu widzenia to abstrakcyjne podejście od sprawy jest najtrudniejsze i zazwyczaj obce mniej doświadczonym projektantom/deweloperom… więc myślę że jeszcze długo będziemy spotykać „modemy” obsługiwane przez dziesiątki klas robiących to samo – bo SOLID tak kazał 😉

  3. @null, raczej problem w tym, że mamy dużo starego kodu. Ja od półtora roku robię przy systemie, w którym średnia długość klasy to około 4000 linii, a pojęcie zmiennej lokalnej nie istnieje i wszystko robione jest na zmiennych globalnych. Rzecz w tym, że kiedyś był to COBOL, którego ktoś magicznie zamienił na javę i teraz wożę się z takim czymś.

  4. Zgadza się, przed tym ma bronić SOLID. Generalnie mi osobiście bardzo ta metodyka pasuje, podobnie jak inne techniki z rodziny Agile. Niestety mam też doświadczenia, gdzie książkowe podejście zabija projekt. Gdzie np do obsługi logowania powoływane są 3-4 klasy, zależnie od tego skąd użytkownik przyszedł, i na tej zasadzie rozpisana każda inna aktywność… czyli takie ewidentne niezrozumienie SRP. Twój przykład zresztą świetnie to prezentuje.
    A co do poprawiania kodu po konwerterach z COBOLA – to współczuję – widziałem jak to wygląda w praktyce… to też jest zresztą argument na wysokie pensje COBOLowców – zwłaszcza w US… 😉

  5. Jeżeli ktoś lubi utrzymywać kod… to niech tworzy klasy. Zazwyczaj źródłem problemu jest nieumiejętne łączenie wzorców i metod programowania.

Napisz odpowiedź

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

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax