W świecie programowania jest wiele zasad, których warto przestrzegać. Zapewne znacie ich sporo. Najpopularniejsze są te z akronimami jak SOLID, DRY, ACID czy KISS. Jest ich jednak znacznie więcej. Istnieją przecież także takie zasady jak spójności kodu czy jego czytelności. Wzorce projektowe także można traktować jak zasady tworzenia aplikacji. Podobnie ma się z uznanymi powszechnie bibliotekami, frameworkami, technologiami, interfejsami czy protokołami. Są też zasady wynikające ze statycznej analizy kodu. Trzeba przyznać ze sporo tego i niełatwo jest o tym wszystkim pamiętać. Myślę, że szczególnie dla początkujących programistów może to być trudne. Kiedy rozpoczynają pracę w swoich pierwszych projektach, to na rewizjach kodu dostają całą masę uwag od bardziej doświadczonych kolegów. Kod, który napisali, musi być wszak dołożony do istniejącego już projektu, a ten musi spełniać uznane standardy. Nietrudno jest się w tym wszystkim zgubić a tym bardziej pamiętać o stosowaniu ich wszystkich. Często ktoś wpada wtedy na pomysł spisania wszystkich tych zasad i dobrych praktyk. Widziałem już wiele takich spisów, lecz jeszcze nigdy nie była tam zawarta najważniejsza zasada pracy programisty. Zdecydowanie najważniejsza i najoczywistsza. Oczywiście, jak to zwykle z principiami bywa, jest to tak ważne, słuszne i oczywiste, że ogrom ludzi o tym zapomina. Odejście jednak od tej zasady pogrąża każdy projekt i to szybciej niż komukolwiek mogłoby się wydawać. Wiele już takich widziałem. Niby to jeszcze żyje, niby jest rozwijane, ale z każdą linią kodu smród zgnilizny czuć coraz bardziej. Ciężko nawet powiedzieć, że taki projekt chodzi. On człapie. Zamiast komunikować się z programistą czy użytkownikiem to coraz bardziej bełkocze, a z czasem już tylko powarkuje. I pożera. Nie tylko zasoby, by trzymać go w iluzji życia, lecz przede wszystkim mózgi. Pożeracz mózgów programistów: projekt-zombie. Pewnie zastanawiacie się, jakaż to zasada trzyma kod przy życiu, a odchodzenie od niej powoduje przeistaczanie się w zombiaka. Zaraz napiszę. Prześledźmy jednak stadium choroby krok po kroku.

0. Stadium początkowe — zdrowy kod

Zacznijmy od początku. Mamy więc zdrowy kod. Może jest niemowlęciem (greenfield), może kilkuletnim dzieckiem, może też być dojrzałym projektem — nie ma to znaczenia. Dopóty stosowana jest nasza najważniejsza zasada, to najprawdopodobniej kod jest w miarę zdrowy. Wiadomo — od czasu do czasu każdemu zdarzy się zachorować. Niektóre choroby są genetyczne — odziedziczone po wcześniejszych projektach. Zdarzają się też wady nabyte, a bywa też, że zaatakuje poważniejsza choroba — czasami nawet śmiertelna — lecz nie zmieniają one jednak naszego pacjenta w zombiaka. Zazwyczaj więc mamy taki w miarę zdrowy projekt, który powoli dojrzewa. Czasami zachłyśnie się jakąś nową ideologią, czasami znajdziemy go pijanego (wiadomo, każdy był kiedyś młody :), ale ogólnie nie jest źle.

1. Stadium zakażenia

Nasz pacjent rozwija się według mądrych zasad a, jak już zdążyliśmy sobie powiedzieć, jest ich sporo i cały czas dochodzą nowe. Ich stosowanie jest niezwykle ważne, ponieważ bez nich zamiast dojrzałego projektu najpewniej doczekalibyśmy się powykręcanego mutanta. Żeby utrzymać kod w ryzach, egzekwujemy je wszystkie. Żadna rewizja nie przejdzie, kiedy znajdzie się jakieś odstępstwo. Bezwzględne trzymanie się zasad — to jest właśnie początek choroby! Zaczyna się niewinnie. Przecież chcemy dobrze, co nie? A co, jeżeli zasady są sprzeczne ze sobą? Zdarza się to częściej, niż moglibyśmy sobie wyobrażać. Na przykład KISS kontra złożone wzorce projektowe, SRP kontra DRY, dziedziczenie kontra zasada podstawień Liskov. Nigdy oczywiście nie są to sprzeczności de iure lecz de facto bywa z tym bardzo różnie. Co wtedy wygrywa? Oczywiście to, co zawsze w sporach ideowych — to, co ma atrakcyjniejszą prezencję! Ja szczególnie wyczulony jestem na zasadę spójności kodu. Kiedy widzę, jak ktoś zaczyna zwracać na nią szczególnie dużą uwagę, to zapala mi się lampka ostrzegawcza. Oho — wygląda na to, że projekt zaraził się zombiozą!

2. Stadium rozwojowe

Zombioza projektowa przypomina zarazę. Początkowo tego nie widać, lecz z czasem rozwija się i to coraz szybciej. We wczesnej fazie można jeszcze ów projekt uratować — wyleczyć bądź wyciąć chorą tkankę a przede wszystkim usunąć jego przyczynę. Jeżeli jednak nie podejmiemy szybko leczenia, to istnieje spora szansa na zgon. Czasami bywa tak, że choroba ta rośnie na tyle wolno, że nie zdąży zabić pacjenta, zanim ten umrze z przyczyn naturalnych (choćby ze starości). Uważam jednak, że nie jest rozsądnym lekceważenie jej z tego powodu. Już w tej fazie bowiem uprzykrza ona życie i rozwój projektu, niszczy ambicję i radość programowania u jego twórców i marnuje pieniądze sponsorów. Zarażony projekt zaczyna coraz bardziej człapać, stękać, warczeć, śmierdzieć i wysysa siły z każdego, kto nim się zajmuje, strasząc całą okolicę. Eutanazja nie jest brana pod uwagę tylko przez przywiązanie i brak alternatyw bądź zasobów na ich zbudowanie. Oczywiście wszystko do czasu.

3. Stadium letalne i zgon

Po kilku lub co najwyżej kilkunastu latach zombiozy ktoś w końcu podejmuje trudną decyzję o litościwym zakończeniu męki takiego projektu. Ze smutkiem, ale i ulgą odcina się go od kroplówki finansowania, odsuwa od niego programistów i składa w trumnie archiwum na cmentarzysku repozytoriów. Mylne jednak jest myślenie, że to koniec problemów z zombiozą. Oj, nie nie — to nie projekt był winien — on był tylko zarażonym pacjentem. Jego śmierć nie oznacza końca pandemii. Zombioza może łatwo zarazić jego następców. Zastanawiacie się czemu tak się dzieje?

Analiza przyczyn zombiozy projektowego post mortem

Zombioza projektowa to potencjalnie śmiertelna choroba przenoszona drogą mentalną. Jej prawdziwa przyczyna leży w głowach nosicieli-programistów. To oni przecież tworzą i rozwijają projekty i to oni je (czasami) zarażają. Jak już wspomniałem, początkiem choroby jest bezwzględne trzymanie się zasad, ale to też nie jest ostateczna przyczyna. Zresztą zombioza może zacząć się też od złych decyzji czy nietrafionych rozwiązań. Właściwą przyczyną tego problemu jest bowiem zapominanie o najważniejszej zasadzie programowania. A co to za zasada?

Najważniejsza zasada programowania

MYŚL! Myśl programując; myśl projektując; myśl przebudowując; myśl wdrażając i myśl planując. Można by sobie wyobrażać, że kto jak kto, ale ludzie zajmujący się zawodowo tworzeniem oprogramowania myślą nad tym, co robią nieustannie. Jest to bardzo naiwne wyobrażenie. Widziałem już wielu programistów, którzy nie zastanawiają się nad tym, co piszą. Zamiast programować — klepią kod. Myśleć podczas programowania to znaczy aktywnie zastanawiać się nad tym, co i po co się pisze — nad sensem, wygodą, praktycznością i wydajnością rozwiązania. Zasady projektowe są wartościową wskazówką i pomocą, ale nie zastąpią samodzielnego myślenia nad kodem. Tak naprawdę to powstawały one w wyniku przemyśleń i doświadczeń pokoleń programistów, lecz nie po to, by zawsze i wszędzie bezwzględnie je stosować. Wiele razy wchodząc w projekt, widziałem bezsensowne rozwiązania, które są notorycznie kontynuowane. Na moje pytanie o przyczynę słyszę wtedy zazwyczaj coś w stylu: "A bo u nas tak już się robi" albo "A bo jest taka zasada / wzorzec / schemat, by tak robić". Od ludzi przerażonych wizją myślenia słyszę zazwyczaj "Musimy zachować spójność kodu w całym projekcie". Otóż nie musimy! Zasady powinny być stosowane wtedy i tam, gdzie ma to sens. Każdy projekt jest inny; ma inne wymogi, technologie, środowisko i zespół, który go rozwija. Program powinien być napisany dobrze — a to znaczy co innego w zależności od kontekstu. Zazwyczaj powinien być on wydajny, rozwijalny, czytelny, bezbłędny i ładny — i będzie taki, jeżeli będziemy myśleć nad tym, co z nim robimy. Kurczowe trzymanie się wszystkich pozostałych zasad spowoduje jedynie nieuchronne zarażenie projektu zombiozą. Wiem, że nie da się myśleć cały czas, że trzeba bazować na przyjętych założeniach, lecz warto notorycznie te założenia weryfikować. Świat się zmienia bardzo szybko — niech nasze głowy też się zmieniają!

Przykłady objawów

Objawów tej choroby jest ogromnie dużo. Zobaczmy sobie kilka prostych przykładów.

Jest to dobra zasada, by metody fabrykujące jakoś sensownie nazywać — tak by wiadomo było, o co chodzi. Taka nazwana metoda lepsza jest wtedy od zwykłego konstruktora. Po co jednak nam takie metody jak "of" czy "from"? Co one wnoszą? Najwyraźniej ktoś dowiedział się kiedyś, że tak należy robić i robi tak nie myśląc nad sensem takiego kodu:

class Example(val value: String) {
companion object {
fun of(value: String) = Example(value)
}
}
fun main() {
Example.of("What the hell is this function for")
Example("when you can simply use a normal constructor?")
}

To samo, kiedy widzę trywialnych budowniczych (builders) dla klas z jednym polem.

To bardzo ważna zasada porządkująca kod i dająca wskazówkę, że projekt klasy idzie w złym kierunku. Czasami jednak warto przekroczyć wyznaczony limit, zamiast obowiązkowo tworzyć potworka, którego trudno jest używać.

Byłem kiedyś świadkiem niezwykłej rozmowy. Jeden programista upierał się, by użyć pełnej ścieżki do klasy adapterowej w kodzie domenowym bez używania importu. Importowanie bowiem klas adapterowych w domenie uważał za sprzeczne z architekturą portów i adapterów. Nieważne, że samo użycie tej klasy w domenie było tak naprawdę jej złamaniem. On usłyszał kiedyś o zasadzie "żadnych importów adapterów w kodzie domenowym" i trzymał się jej, nie myśląc w ogóle nad jej sensem. Na zarzut, że to tylko maskowanie niedozwolonych zależności stwierdził, że zamiast tworzyć zmienną, użyje łańcuszka wywołań — wtedy "ominie" import niedozwolonej zależności. W tym momencie się poddałem...

public class Playground {
public static void main(String[] args) {
DomainClass example = new DomainClass();
adapter.AdapterType dependency = example.getDependency(); // hmm, how to hide that we use adapter's dependency?
String value = example.getDependency().getValue(); // simply do not assign it to variable, right? ;)
}
}

To, że w jednym projekcie stosowana jest Kafka, nie oznacza, że w innym nie warto użyć np. RabbitMQ. A może kolejny mikro-serwis napisać w Go zamiast na JVM? Czy JPA będzie wszędzie najlepszym sposobem na używanie bazy danych? A może czegoś nie warto w ogóle pisać? Nie mam na myśli oczywiście tego, by bezmyślnie używać coraz to innych rozwiązań — wtedy będziemy mieć tylko bałagan. Mam na myśli to, by z głową podchodzić do nowych wyzwań i być na bieżąco!

Jednym z najnowszych przykładów zombiozy, który widziałem, jest eitheroza maniakalno-kompulsywna. Biblioteki takie jak Vavr czy Arrow to potężne narzędzia — wszyscy je chwalą. Tak, tak — jak nie stosujesz takich monad, to jesteś zapóźniony :)
— Pobierasz coś z repozytorium — zwracasz either i przepychasz go aż do kontrolera!
— Ale to nie ma sensu...
— Sam jesteś bez sensu — tylko either!
— Ale czym to się wtedy różni od throws?
— Either! Używaj either! Tak trzeba!
— Ale nie wykorzystujemy wtedy eithera tak, jak powinno się go wykorzystywać i nie używamy w ogóle jego mocy a ponosimy koszt mniejszej czytelności kodu!
— Either! Wrrr! Ma być either! Either zawsze! Either wszędzie! Mówili żeby używać!
— ...

Dobra praktyka na koniec

Na końcu warto wspomnieć o jednej dobrej praktyce, dzięki której kod naszego programu staje się znacznie bardziej zrozumiały, a inni programiści nie podążają ślepo za nami. Uzasadnij swój wybór danej zasady, praktyki czy wzorca! Można to zrobić w komentarzu, pliku readme czy na stronie w Confluence. Nazywa się to ADR (Architecture Decision Record) i jest bardzo dobrą profilaktyką antyzombiozy. Kiedy inni programiści przeczytają, w jakich warunkach powstawała dana decyzja, to może mniej będą skłonni do ślepego za nią podążania. Kiedy warunki się zmienią to większa jest wtedy szansa że przemyślą rozwiązanie raz jeszcze :)