Dobre rady pana ojca

czyli porady zebrane w trakcie 20+ lat doświadczenia z informatyką.

Ten artykuł może być od czasu do czasu aktualizowany, dlatego walnij sobie zakładkę i wracaj tu średnio raz na 2 miesiące. Usunę ten disclaimer jak uznam, że jest skończony.

Artykuł pisany jest również ze sporym zboczeniem backendowym, ale nawet frontendzi znajdą tu coś dla siebie.

Testy jednostkowe

Próbowałeś się kiedyś nauczyć języka nie wypowiadając w nim ani słowa? Podobnie też działa próba pisania jakiegokolwiek frameworku sieciowego (tutaj to widać najbardziej) czy w ogóle jakiegokolwiek softu, który nie jest z definicji self-contained. Dlatego odpuść sobie testy jednostkowe, chyba że pracujesz nad hardkorową algorytmiką, która jest self-contained! Coraz mniej systemów jest takich, a mockowanie połowy wszechświata mija się z celem. Twoim zadanie jest przetestowanie oprogramowania, a nie pisanie ułomnej implementacji połowy Postgresa, do którego zapytania wysyła twoja mądra apka.

Nie mówię że testy jednostkowe są zupełnie użyteczne, ale w krajobrazie 3ciej dekady XXI wieku wydają się nieco tracić na znaczeniu.

Pisz zamiast tego testy integracyjne. W ostatnich czasach bardzo zmalała trudność postawienia na ich potrzeby Postgresa, czy nawet Cassandry. Można to robić od biedy w sposób totalnie zero-effortowy, na przykład za pomocą docker-compose. Co prawda przysparza to pewnych problemów w momencie kiedy chcesz scancellować build, ale to nic z czym Twój lokalny SRE nie będzie sobie w stanie poradzić (kontenery z docker-compose odpalone dłużej niż 24h? Kill’em).

Zresztą, ta sama funkcjonalność testowana dwukrotnie, najpierw przez testy jednostkowe, a potem przez integracyjne czy E2E brzmią jak najszczersza bzdura na świecie.

Komentarze

Kiedyś przeglądając Internet natrafiłem na stwierdzenie self-explaining source isn’t. Dlatego zakazywanie ludziom pisania komentarzy podpada co najmniej pod Konwencję Genewską.

Mój złoty standard na pisanie komentarzy jest następujący: czy za pół roku będę miał pojęcie, co tutaj odwaliłem? Jeśli odpowiedź jest negatywna, wrzuć komentarz. Pewien alternatywny standard tyczy się mindsetu maintenance programmera. Ten człowiek nie ma najmniejszego interesu w zrozumieniu pełni błyskotliwości Twojej ręki. On chce wprowadzić zmiany, zacommitować je i pójść do domu. Dlatego też cecha, która w dzisiejszych czasach jest prawie na wymarciu: empatia jest niezwykle istotna. Czasem może być to jednak empatia do samego siebie. Wierzę w to, że nawet psychopaci są w stanie skrzesać z siebie takie uczucie.

Pytanie czy za te pół roku sam zrozumiesz co napisałeś, zwłaszcza jeśli właśnie odwaliłeś coś, co w twoich oczach wydaje się być szczególnie mądre, czy też cwane. Dlatego zrób sobie przysługę już dzisiaj i zacznij wstawiać te komentarze, nawet jeśli dla ciebie są one odrobinę redundantne.

Co robi dany kod powinno być oczywiste w momencie w którym nań zerkasz. Chciałbym w tym momencie trochę nawiązać do Zen of Python: “There should be one– and preferably only one –obvious way to do it.“.

Jeśli Twój kod ma potencjał rzucić jakimś rzadkim wyjątkiem, to powinno się to znaleźć w dokumentacji, jeśli nie w komentarzu.

Empatia c.d.

Ostatnio junior przyszedł do mnie z pewnym problemem. Na skutek pewnego nieszczęśliwego splotu wydarzeń endpoint pt. “zwróć listę użytkowników” napisał tak, że jeśli lista nie była pusta to zwracał 200 i listę, zaś jeśli była pusta to zwracał 204 No Content.

Nie było to do końca niepoprawne. Gdyby był moim studentem, nie mógłbym go uwalić. Co najgorsze, konsultacja z kolejnym moim przyjacielem, też junior developerem, stwierdziła że on zachowałby się dokładnie tak samo. Podniosłem więc następujące argumenty (w kolejności istotności malejąco):

  1. Gdyby miał napisać funkcję get_user_list() to czy w obecności pustej listy zwróciłby nulla?
  2. Empatia dla frontenda w stopniu zasadniczym:
    • Ten człowiek teraz będzie musiał explicite sprawdzać czy nie dostał przypadkiem 204 i podstawić sobie zamiast tego pustą listę, o czym dowie się zapewne w momencie kiedy pierwszy raz dostanie 204 przez twarz i zastanowi się “WTF”. Wtedy z lupą wróci do twojej dokumentacji i lepiej niech to tam wisi, dużymi literami.
    • Frontend najpewniej testować będzie kod w taki sposób, że przetestuje go dla niepustej listy, a pustą listę zupełnie pominie, co wprowadzi subtelne błędy w jego kod
    • Taki sposób programowania to zastawianie na przyszłego użytkownika miny przeciwpancernej. Jak raz mu wywali prosto w twarz i uszkodzi coś przy okazji, twoje API będzie egzaminowane z wykrywaczem min.
    • Frontend do końca twoich dni będzie miał cię za osobę z poważnymi problemami natury psychicznej.
  3. Musisz to ekstra odnotować w dokumentacji.
  4. API jest jak biblioteka. Powinna być wzorem prostolinijności, tak żeby po prostu nie było się do czego przyczepić (przynajmniej na pierwszy rzut oka).
  5. Zapytania typu GET nie powinny co do zasady kończyć się 204kami!

Mam jeszcze pewne zastrzeżenia co do uprawiania programowania funkcyjnego jak powalony: zgadnijcie jak czyta się stack trace’y z poczwórnie zagnieżdżoną lambdą? Jeśli nie musicie, dajcie sobie spokój. Czytelność powinna grać w kodzie pierwsze skrzypce, dopiero potem wydajność/elegancja implementacji. Zaś o zaletach czytelności kodu uwidocznionej przez narzędzia debugujące czy stack trace’y nie muszę pisać, bo są one oczywiste.

Po co coś robię?

Zanim przystąpisz do wykonania taska, upewnij się że rozumiesz jego kontekst biznesowy, czyli po co ją robię. Oczywiście, możesz zrobić tylko to co opisano w tasku i być krytym, ale wtedy będziesz programistą co najwyżej poprawnym, ale nie dobrym. Choć oczywiście może to być jedyny sposób radzenia sobie z toksycznym środowiskiem pracy w momencie kiedy architekt jest lekko upośledzony, ale zakładamy że z takich środowisk dość szybko wymiksowujemy się, zamiast stosować strajk włoski, chyba że płacą zbyt dobrze.

Zróbmy przykład: miałem taska że do trace’a w Jaegerze trzeba było dodać kilka tagów. Operacja cała trwała nawet i 20 minut, więc przy testach z zdziwieniem stwierdziłem, że dostaję od Jaegera 404kę. Ale po chwili zreflektowałem się: oczywiście że tak jest, operacja trwa 20 minut i dopóki dana operacja, do której załączyłem te tagi nie zostanie zakończona, dopóty nie zostanie ona w ogóle przesłana do Jaegera, tak więc wyszukiwanie Dodałem więc killer feature: emisja zerowego spana, który zawierałby te dane w tagach. Słowem, dodając je do pustego, emitowanego natychmiast spana, mogłem umożliwić użytkownikom wyszukiwanie po treści tego taga (atrybutu wg nomenklatury OpenTelemetry) natychmiast, a nie gdy cała 20-minutowa operacja reprezentowana spanem, do którego miałem przywalić tagi się skończy. Oczywiście nadal trzeba czekać na zakończenie się operacji, aby uzyskać całośc informacji, ale przynajmniej Jaeger już nie wali 404ką. Reasumując: dawaj z siebie 120%, rozszerz nawet jakąś funkcję jeśli jest to wskazane i jeśli to wynika z kontekstu. Masę problemów i przeszkód wynika po prostu z braku zrozumienia tematu, lub faktu że ktoś coś po prostu przeoczył. Miej baczenie na takie sytuacje – zachowaj elastyczność myślenia.

Pamiętaj, że jesteś samodzielnym zawodem informatycznym i masz tak jak pielęgniarka w szpitalu prawo odmówić lekarzowi wykonania podanej operacji, ponieważ uważasz że jest głupia/niepotrzebna/whatever i to w tym momencie zadaniem architekta jest Ci to wyjaśnić lub przyznać się do błędu, z czym w kulturze blameless nikt nie ma problemu. Masz rozumieć, w jakim celu piszesz ten kod, co starasz się tym osiągnąć. Jesteś ostatnią linią obrony przed tzw. reality 101 failures, czyli sytuacjami w którym okazuje się że funkcja została wykonana zgodnie ze specyfikacją, ale jest nikomu niepotrzebna lub została źle zrozumiana.

Kieruj się również boy scout rule – “always leave the place in a better state than you found it”. Dopisuj komentarze w kodzie, czasem zupełnie awansem, nawet przy kodzie którego Ty nie napisałeś. Niech Cię to nie odciąga od rzeczy ważniejszych – wierzę że będziesz w stanie użyć swojego better judgment by ocenić, które z tych sytuacji będą dla Ciebie dobre.

Defensywne asercje, fail-fast i obrona w głębi

Większość języków programowania ma mechanizm tzw. asercji. Są to proste testy mające na celu stwierdzenie czy algorytm zachował przyczepność z rzeczywistością, czy w ogóle odleciał.

Oburzyć się można: ale jak to? Przecież takie durne testy spowolnią mój program. Nie tak szybko. Większość interpreterów (czy nawet kompilatorów) ma sposób na programowe wyłączenie asercji.

Asercje przy okazji są też sposobem na wyrażanie pewnych inwariantów (relacji które nie podlegają zmianiei są względnie stałe w czasie wykonywania danego algorytmu) w językach programowania, które nie mają explicite składni do wyrażania tychże.

Czyli tl;dr – łap błędy zanim kaskadują. Im szybciej złapiesz danego buga, tym szybciej załatasz swój kod i tym mniej pytań będzie do jego zachowania (jakość kodu ponoć mierzy się w WTF na minutę).

Wracając jeszcze do programowania defensywnego – zakładaj różne dziwne rzeczy, załóż na przykład że niektóre wątki nie zostaną uruchomione bo RuntimeError, zwłaszcza na granicy systemu operacyjnego i twojego programu. Zwalnia cię to poniekąd z obowiązku googlowania “mój ulubiony syscall man”. Przytrafiła mi się taka sytuacja, w niesłychanym wręcz splocie wydarzeń w który i tak nie uwierzycie, w której nowe wątki nie były w stanie się uruchomić. W każdym razie, teraz twoja aplikacja uruchomi się bez tych nie niezbędnych wątków w zredukowanej funkcjonalności, ale to zadziała. Jeśli jakaś część aplikacji jest niekrytyczna, powinna sobie poradzić bez niej. Powodów takiego RuntimeErrora przy uruchamianiu wątków mogą być setki i wykracza to poza zakres tego artykułu. Ogranicz się jednak, aby nie łapać tych wyjątków w sposób pokemonowy (gotta catch’em all). Słowem – wiedz co łapiesz (choć uzasadnione podejrzenie wystarczy), choć mogą to być rzeczy nieoczywiste. Małym błędem jest łapanie za dużo, natomiast dużym jest łapanie zbyt mało – zwłaszcza w językach, w których nie wiesz co dokładnie zostanie rzucone, bo nie weryfikuje tego kompilator (np. Python, w szczególności nie Java).

Mowa tutaj również o sprawdzaniu czy konkretne wywołanie wywoła się tylko raz, zwłaszcza w bibliotekach i interfejsach API. Co się stanie, jeśli nasz nowy deweloper przypadkowo wywoła wywołanie dwa razy (co implikuje błąd logiczny) lub z przyczyn czysto runtime’owych wywołanie wywoła się więcej niż raz? Piszmy obronnie, zwłaszcza jeśli nasz kod jest wrażliwy na kolejność wykonywania, albo robi jakieś dzikie rzeczy. Czyli nie zakładaj że świat ma sens przed wykonaniem jakiejś operacji – sprawdź to!

Pragnę zwrócić uwagę jednocześnie na podejście typu crash-only software. To oprogramowanie, które można zatrzymać tylko na jeden sposób – prowokując awarię, lub zabijając je jakimś SIGKILLem. Podejście to ma dużo więcej sensu w momencie, kiedy Twoimi aplikacjami zarządza jakiś Docker Swarm czy inny Kubernetes. Program dostanie wyjątek, który nie bardzo wiadomo jak zhandlować? Niech strzeli sobie w łeb, a Docker wskrzesi go w dniu ostatnim.

Wykształcenie współpracowników

Generał Korpusu Marines Stanów Zjednoczonych powiedział kiedyś: “Każdy Marine jest przede wszystkim strzelcem. Wszelkie pozostałe warunki są drugorzędne”. Umożliwia to nawet kucharzom USMC wzięcie do ręku karabinu i strzelanie, w razie “W”.

Ja wyszedłem z założenia że w SMOK-u każdy deweloper jest backendem i po części również administratorem. Ponieważ najwięcej kodu jest przy backendzie i często trzeba tam coś zmieniać, wyposażenie teamu w takie kompetencje pozwoli najszybciej reagować na problemy. Nierzadko również kod źródłowy jest jedyną dokumentacją (czy to przez roztargnienie czy celowo), dlatego tak ważny jest punkt Komentarze, tak aby nawet dla juniora na pierwszy rzut oka było oczywiste jak dany kod działa i jakie zachowanie podejmie w wyniku przekazania mu konkretnych argumentów. Miałem kiedyś nawet pomysł żeby stworzyć język programowania, w którym dokumentacja byłaby pomieszana z kodem instrukcyjnym – nawet do punktu załączania JPGów i PDFów do kodu źródłowego. Powstało sporo inicjatyw, takich jak na przykład Natural Docs czy chociażby literate programming pana Knutha.

Kolejna uwaga. Prowadź przykładem. Jeśli chcesz żeby Twoi współpracownicy refaktorowali kod, refaktoruj go jako pierwszy. Niech się zacznie od ciebie. Bardzo inspirujący pod tym względem jest artykuł pt. “Dwadzieścia rad dla rzemieślników z 1904 roku”, zwłaszcza pozycja nr 18.

Rób tak, żeby było dobrze

Kiedyś w kodzie natrafiłem na miejsce w którym endpoint typu DELETE przyjmował ciało zapytania. Zgodnie z RFC 9110:

Although request message framing is independent of the method used, content received in a DELETE request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack

Więc co zrobiłem? Wypisałem w komentarzu sążnisty tekst o tym, że takie zachowanie nie powinno w ogóle mieć miejsca, ale zdecydowałem się pozostawić opcję nadsyłania argumentów za pomocą body. W końcu interfejs już zaczął być eksploatowany w ten sposób, a poprawianie wszystkiego byłoby zbyt wielką robotą. Dokumentację poprawiłem w taki sposób, żeby jedynym zalecanym sposobem przekazywania argumentów były query args, czyli ?mniej=więcej&w=taki&sposób=1, ale interfejs zje również i body, zgodnie z zasadami robustness principle.

I tutaj przestroga dla osób wszystkich, nie tylko związanych z IT – rób tak, żeby było dobrze. Nikt nie chce się grzebać w procedurach czy specyfikacjach for the sake of it. Niekoniecznie w swoje zmiany musisz angażować architekta i połowę zespołu. Zrób tak, żeby wszyscy byli uśmiechnięli i mogli pójść szybko do domu, bez zbędnych przebojów. Wykorzystaj tutaj swoje wyczucie – czy dana procedura czy specyfikacja jest naprawdę warta darcia o to kotów?

Published

By Piotr Maślanka

Programmer, paramedic, entrepreneur, biotechnologist, expert witness. Your favourite renaissance man.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.