Dbajmy o siebie! Czyli rzecz o kompatybilności
Cześć!
W ostatnich dniach przez wszystkie przypadki odmieniane jest słowo odpowiedzialność. Odpowiedzialność za nas, za innych. Jesteśmy w tej wygodnej sytuacji, że możemy po raz pierwszy być bohaterami nie nosząc peleryny tylko siedząc na dupie. No i faktycznie warto jeszcze bardziej niż zwykle zadbać o naszych najbliższych i o nas samych. Nawet nic nie robiąc. Możemy też zadbać o nasze koleżanki i kolegów z pracy. Często śmieję się, że najlepszą formą pracy w korporacji jest nic nie robienie. Praca w korpo to jest taki rodzaj pracy, gdzie za dobro się nie wynagradza a za zło każe. Jeśli nie zrobisz nic dobrego to nikt Cię nie pochwali, ale jak zepsujesz próbując, to już podpierdółka na asapie u przełożonego na mailu leci.
Oczywiście nie namawiam nikogo do nic nie robienia w pracy, ale już jak najbardziej namawiam do zastanowienia się nad efektami swojej pracy. Czasem zrobienie mniej lub wolniej przynosi lepsze i szybsze rezultaty niż zrobienie czegoś “na szybko”. Programista wg mnie powinien pracować bowiem wobec powyższych trzech reguł:
- Primum Non Nocere (dokładnie - tak jak lekarz),
- Work smart not hard,
- Less is more.
Do czego zmierzam?
Podstawą pracy w zespołach, szczególnie rozproszonych jest poszanowanie kompatybilności (compatibility). No i pewnie możesz powiedzieć, no raczej, robię migrację w Entity Framework, zachowuję kompatybilność wsteczną.
No i faktycznie termin backward compatibility, czyli kompatybilności wstecznej jest najbardziej znany. Tylko czy faktycznie rozumiany? Literalnie oznacza on, że nasze nowe zmiany będą poprawnie działały z aktualnym (czy tez poprzednim) stanem aplikacji. Co to w praktyce oznacza? Czy migracje są kompatybilne wstecz? Nie bardzo, jest to transformowanie poprzedniej wersji aplikacji do nowej - czyli nasze zmiany nie są kompatybilne wstecz, one po prostu zmieniają poprzedni stan tak aby był zbieżny z nowym. Dlaczego to nie jest zgodne? Wyobraź sobie, że skalujemy nasz model wszerz - czyli mamy kilka instancji tego samego serwisu (aby zapewnić lepszą dostępność). Mamy tzw. rolling update, czyli wdrażamy nową wersję instancja po instancji - bo np. chcemy zachować jak największą dostępność. No i mamy sytuację, że pierwsza wdrożona instancja zmigruje bazę danych. Co się stanie z pozostałymi instancjami? Co jeśli im load balancer przydzieli wykonanie żądania? No właśnie.
Inna sytuacja to robimy bibliotekę “core’ową”, którą używają inne biblioteki i stwierdzamy, że “a w sumie dodam sobie tutaj jeszcze parametr do tej metody”. Niby niegroźna zmiana, ale co się stanie jeśli uczynimy ten parametr wymaganym (bez wartości domyślnej) i ktoś zainstaluje sobie nową wersję naszego pakietu? No właśnie
Albo taka sytuacja, że mamy system oparty na zdarzeniach i zauważyliśmy, że w naszym kontrakcie była literówka. Wiadomo, niezręczne przejęzyczenie to nic fajnego. Zmieniamy więc UserAnme na UserName. Wypuszczamy nową wersję i publikujemy już poprawione zdarzenie po dodaniu użytkownika. Tylko czy ono nie jest przypadkiem “poprawione”? Co się stanie jak inny (mikro)serwis będzie na nie nasłuchiwał? Co się stanie jeśli zamiast pola, które się spodziewał otrzyma inne (nawet ładniej wyglądające)? Żywy człowiek sobie to dopasuje, ale deserializator? No właśnie.
Przykładów możnaby mnożyć. Wszystkie te scenariusze mogą nie zostać wychwycone przez testy bo one zwykle testują stan aktualny. Wtedy tylko można się zapytać “to skoro jest tak pięknie, to czemu jest tak źle?“.
Powiesz może “o jeju to sobie to poprawi, to jest 5 minut roboty”. No tylko, że może to jest 5 minut roboty - dla Ciebie. Nawet jeśli ten ktoś będzie o tym poinformowany i zabierze się za to to nigdy nie jest 5 minut roboty. Dolicz do tego przełączenie kontekstu, napisanie kodu, zweryfikowanie go, poprawka testów, sprawdzenie integracyjne czy to faktycznie działa, potem pull request, code review, dolicz jeszcze, że kilka osób będzie przeglądało ten kod i dyskutowało itd. itp. Dalej 5 minut wychodzi? A mówimy o scenariuszu optymistycznym, nie o scenariuszu gdy ktoś dowiaduje się w momencie jak dostaje błąd z produkcji, bo “coś się źle zapisuje”. Ile wtedy zajmie? No właśnie.
Jeff Sutherland w swojej książce “Scrum: The Art of Doing Twice the Work in Half the Time” (którą mocno polecam) przywołuje badania przeprowadzone w firmie Palm:
*"They looked at the "Matts" across the entire company—hundreds of developers—and they decided to analyze how long it took to fix a bug if they did it right away versus if they tried to fix it a few weeks later. Now, remember, software can be a pretty complicated and involved thing, so what do you think was the difference?*
*It took twenty-four times longer. If a bug was addressed on the day it was created, it would take an hour to fix; three weeks later, it would take twenty-four hours. It didn’t even matter if the bug was big or small, complicated or simple—it always took twenty-four times longer three weeks later. As you can imagine, every software developer in the company was soon required to test and fix their code on the same day."*
Trochę oszukałem z tymi trzema regułami. Tak naprawdę to jest tylko jedna: NIE RÓB BREAKING CHANGE I NIE PSUJ INNYM PRACY. NIGDY! Brzmi radykalnie? Być może, ale jest prawdziwe, naprawdę zawsze jest alternatywa.
A co jeśli Ci powiem, że komaptybilność wsteczna to nie wszystko? Że mamy jeszcze coś takiego jak forward compatibility (kompatyblność wprzód)? Oznacza ona, że musimy przewidywać, że nasz kod może być wywoływany przez nową wersję kodu/mikroserwisu. Możnaby powiedzieć, że to taka kompatybilność wsteczna tylko widziana z perspektywy tego starego kodu/stanu. Oznacza to np. że jeśli nasłuchujemy na jakieś zdarzenie np. DodanoUżytkownika, które ma pola Id, NazwaUzytkownika to możemy się spodziewać, że np. w przyszłości przyjdzie w nim jeszcze nowe pola np. Email, DataUrodzenia, itd. Oczywiście nie przewidzimy w pełni jak nasza aplikacja będzie się rozwijać, nie obsłużymy pól, których nie znamy. To nie znaczy, że musimy wszystko przewidzieć, ale to co musimy mieć w głowie to, że kontrakt może się zmienić i jeśli będzie on kompatybilny wstecz (czyli z tym, który znamy) to powinniśmy go obsłużyć. Jak to możemy zrobić? Ano możemy przykładowo nie wywalać błędu jak przyjdą nam dodatkowe pola. Jeśli np. robimy event sourcing i zapisujemy zdarzenia do bazy w postaci JSON to powinniśmy je zapisać takie jakimi je otrzymaliśmy a nie tylko wycięte. Dzięki czemu jak zaktualizujemy kod, to będziemy mogli nawet na bazie tych danych potem odtworzyć nową logikę w kolejnej wersji naszego serwisu.
Także pamiętajmy. Pomagajmy sobie i innym. Starajmy sie nie psuć innym roboty, bo to naprawdę się opłaca - dosłownie. To naprawdę nie jest strata czasu.
Jak to wygląda u Ciebie? Co o tym sądzisz? Robisz breaking change czy nie robisz? Czekam na Twoją odpowiedź!
Pozdrawiam i zdrówka życzę!
Oskar
p.s.2 Nie wiem czy kącik muzyczny Ci przypadł do gustu, ale póki co go będę kontynuował. Oto i piosenka o poruszaniu się wstecz: