Przemyślenia mnie naszły na podstawie tego postu i komentarzy pod nim oraz na podstawie zalinkowanego tam https://wiki.c2.com/?FearOfAddingClasses .
Do jednego się muszę przyznać: w swojej karierze dotąd praktycznie nie zdarzało się, bym musiał pracować z kodem kogokolwiek innego, niż swoim własnym. W konsekwencji jestem przyzwyczajony do ogarniania w pełni całego projektu, na którym pracuję. Pewnie wpływa to na moją perspektywą.
W rzadkich wypadkach, gdy musiałem pracować z czyimś kodem, kończyło się na tym, że przepisywałem ten kod, by go ogarnąć. Albo po prostu nie tykałem się go.
Znana jest częsta preferencja licznych, ale bardzo krótkich funkcji. Uncle Bob doprowadza ją do ekstremum, promując, że jeśli tylko funkcję/metodę da się sensownie podzielić, trzeba ją podzielić. Jak się domyślam, cel jest taki, by kod był samodokumentujący się: nazwy metod robią za komentarz do tych dwóch-trzech linijek kodu, który ta metoda ma zawierać.
Ja, przyznam się szczerze, nie lubię tego podejścia. Wolę dłuższe metody. Nie ekstremalnie długie, powiedzmy pół - dwa ekrany na długość? Powód jest prosty: Jeśli już muszę czytać kod, to interesuje mnie, co ten kod robi, a niekoniecznie to, co programista (najczęściej ja) miał na myśli, że ma robić. Liczne metody sprawiają, że logika staje się rozkichana, trudno ją prześledzić. Trzeba ciągle skakać od metody do metody.
Tak się zastanawiam... Czy nie ma tutaj dwóch, zupełnie różnych, przeciwnych w zasadzie podejść do prostoty?
- Podejście pierwsze, naiwne, charakterystyczne dla początkujących i chyba także - co trochę paradoksalne - do programistów naprawdę starej daty ("old Unix guy"): Kod musi być możliwy do ogarnięcia w całości przez jednego człowieka. Wskutek tego będziemy zmniejszać abstrakcje do niezbędnego minimum. Mało klas, o ile w ogóle będą klasy, bo z tym podejściem zazwyczaj wiąże się odrzucenie OOP, no ale w niektórych językach muszą być klasy, więc trochę ich, siłą rzeczy, będzie (chociaż z OOP dalej nie będzie miało to wiele wspólnego). Minimum zależności, najchętniej tylko te, które są naprawdę konieczne albo powszechnie znane. Naginanie wymogów biznesowych do tego, co dyktuje architektura programu, wrogość wobec mnożenia ficzerów. Także preferencja do mniejszej ilości funkcji, za to większych - nie do przesady jednak: tak, by funkcja była na tyle duża, by można było prześledzić bieg logiki bez skakania po kodzie, ale znowu na tyle mała, by ten bieg logiki nie był na tyle zamotany, by nie mieścił się w głowie.
Rzadko się obecnie spotyka ludzi promujących to podejście, ale jednak są wyjątki. W bardzo radykalny sposób promują je ludzie stojący za stroną https://cat-v.org/ . Ich poglądy są tak absurdalne jak na obecne czasy, że lektura ich strony przypomina groteskę czasem.
- Podejście drugie, obecnie uważane za profesjonalne i jedynie słuszne: Cała aplikacja nie będzie możliwy do ogarnięcia przez jedną osobę, wobec czego musi być zaprojektowany tak, by możliwe było zajęcie się jakąś jego częścią, mając tylko bardzo ogólne i niejasne pojęcie o całości. To jest, w praktyce, podejście dokładnie przeciwne do poprzedniego. Tutaj będziemy właśnie mnożyć abstrakcje, by zapewnić tę własność. Liczne drobniutkie funkcje utrudniają dokładne prześledzenie control flow, ale właśnie nikt ma tego control flow nie śledzić dokładnie, tylko zadowolić się nazwami funkcji, pokazującymi intencję stojącą za kodem, bez konieczności odszyfrowywania tej intencji z control flow i z komentarzy. Liczne abstrakcje, klasy i wzorce powodują eksplozję LoC, ale za to maja w założeniu zagwarantować lokalność koniecznych zmian i łatwość w rozszerzaniu aplikacji. Taki był chyba zamysł OOP, chociaż teraz promuje się (z tych samych powodów zresztą) FP oraz mikroserwisy. FP narzuca przestrzeganie ograniczeń, które są dość niewygodne dla początkujących (niemutowalność bywa uciążliwa, póki sie do niej nie przyzwyczai), które jednak gwarantują - i to w sposób możliwy do wykazania matematycznie - te właśnie pożądane cechy: lokalność zmian (na działanie funkcji wpływa tylko jej własny kod i argumenty, nie dowolne inne partie programu) i kompozycyjność (można łączyć funkcje bez zbytniego przejmowania się, co mają w srodku). Wreszcie mikroserwisy doprowadzają to do ekstremum: kosztem kolejnej eksplozji w LoC gwarantują jeszcze bardziej rygorystyczny podział całości na lokalne części.
Przypuszczam, że w praktyce będzie jeszcze trzecie podejście, charakterystyczne dla nieco podszkolonych newbie (architecture astronaut): Jak już człowiek się wyleczy z tego podejścia pierwszego, ale jeszcze nie rozumie drugiego, to produkuje potworki. Wali wzorce bez ich zrozumienia. Mnoży abstrakcje, które mu wydaja się eleganckie, ale nic nie wnoszą poza komplikacją. Stosuje FP, które polega na przekazywaniu parunastu argumentów do każdej funkcji, z których jeden to rozległa struktura zawierająca cały stan programu. Dzieli program na mikroserwisy powiązane tak silnie, że i tak trzeba ogarniać całość.
Czyli, to co najgorsze z obydwu światów? Kod niegwarantujący tego, że można patrzeć tylko na jeden jego wycinek (w zasadzie gwarantujący coś dokładnie przeciwnego), a z drugiej strony tak duży (zarówno pod względem LoC, jak i ilości klas / abstrakcji / czego tam jeszcze), że nie da się ogarnąć go w całości.
Muszę uważać, bo mi to trzecie podejście realnie grozi... Jeśli już w to nie wpadałem czasem.
Z drugiej strony, do naprawdę małych / jednoosobowych projektów to pierwsze podejście wydaje się być praktyczniejsze.
Jednak realistycznie rzecz biorąc wypadałoby się wreszcie nauczyć tego drugiego...