Istniejący kod klienta? NIE PRZEPISUJ
Tytuł tego tekstu i cały jego zamysł powstał przy okazji realizacji zadań w projekcie klienta w pracy. Może się wydawać, że pokazuje smutną rzeczywistość i walkę programisty z tym złym biznesem. Ale czy na pewno? Czy na pewno jest to walka? Czy jako programiści, konsultanci, specjaliści w swoim fachu powinniśmy uznawać przedstawioną tutaj sytuację za sprzeczną z „prawidłowym” podejściem? Zaprzeczającą jedynej słusznej drodze? A może po prostu wielu z nas dalej tkwi w programistycznej utopii. W takie, gdzie kod napisany przez nas jest jedynym słusznym. A to co powstało kiedyś, w dodatku zrobione przez kogoś innego to na pewno jest złe? Może wielu z nas tkwi w projektach prowadzonych w „warunkach laboratoryjnych”. Gdzie mamy pełną kontrolę nad sytuacją? O tym porozmawiajmy sobie dzisiaj.
Nie przepisuj. Wpasowuj.
Źródło zła
Zacznę może od zarysowania sytuacji i umieszczenia bohaterów na scenie. W końcu coś nas tu sprowadziło i warto wiedzieć co to było i dlaczego wywołało tyle emocji.
Aktualnie w pracy ja i kilku kolegów jesteśmy związani z projektem, który w większości powstawał i powstaje po stronie klienta. Jednak naszym pierwszym zadaniem było dopisanie osobnego API dla jednego z modułów. Reszta to monolit zarządzany właśnie przez klienta. Po skończeniu API przenieśliśmy się do tego monolitu właśnie. Jako, że podobno jesteśmy profesjonalistami, a przynajmniej płacą nam jakbyśmy nimi byli, to nie protestowaliśmy i wskoczyliśmy żwawo w istniejący projekt i bieżące zadania. Żadnego bawienia się w poprawianie literówek dla zapoznania się z kodem. Takie rzeczy zostawmy stażystom. I aktualnie jedno ambitniejsze zadanie trwa. Ale zanim do niego przejdziemy to wróćmy do API.
API nasze wymuskane
Programiści lubią pisać kod w nowym projekcie. Takim prosto z salonu, pachnącym folią na tapicerce. Wtedy możemy się poczuć Panami. Władcami wszelkiej materii i tymi, którzy narzucają warunki. Nawet jak ostatecznie robimy CRUD, który jest tylko dlatego, że głupio było wywoływać SQLa w Angularze
I taki projekt dostaliśmy. Pachnące nowością API. W dodatku w technologiach, których wpisanie w CV nie powoduje, że będzie ono z drukarki wpadało bezpośrednio do niszczarki pod nią.
Pojechaliśmy do klienta. Podyskutowaliśmy, uznaliśmy się sami za Panów kodu i Oświeconych więc kilka razy ktoś z nas zasugerował klientowi, że się myli. I wróciliśmy aby zabrać się za szerzenie dobrej nowiny i zapełnianie dysków serwera, na którym leży repozytorium.
API odpowiedzialne jest za obsługę danych łączących klienta z wymaganymi przez prawo dokumentami i datami ich wprowadzenia do systemu. Do tego akceptacja przez inne osoby wprowadzonych zmian w tych zapisanych danych.
Ktoś może mi zarzucić, że piszę w liczbie mnogiej opowiadając o tym co myślę. Na szczęście zdarza mi się odzywać do ludzi, a ci z którymi pracuję nawet mi czasami odpowiadają przekazując co sądzą na pewne tematy. Więc czuję się usprawiedliwiony.
I API powstało. Dumnie zaprezentowany został na wszelakich mitingach kawał kodu pchający encje z bazy do JSONa i z powrotem (kod tak bardzo nie miał logiki, że musieliśmy pisać testy E2E żeby cokolwiek testować poza tworzeniem obiektów). Cel osiągnięty. API gotowe. W dodatku z użyciem nowoczesnych bibliotek i narzędzi. Do tego kod ułożony elegancko i nowocześnie. Pojedyncze odpowiedzialności. Podział na podprojekty. Wstrzykiwanie zależności. Podział endpointów na komendy i zapytania. Sporo testów, automatyczne budowanie po commicie itd.
Czego znowu potrzebujesz?!
Idealna robota, której nic nie ruszy. Gotowa na wszystko, prawda? Prawie. Bo kiedy API zaczęło być używane to pojawiały się potrzeby zmian, momentami przeczące dobrym praktykom, pojedynczym odpowiedzialnościom i zasadą wzorowego REST API.
Frustracja się pojawiła i emocje. I zarzucanie głupoty różnym osobom. I oczywiście to oni są ci źli, którzy mieszają. Wiadomo.
Ale w końcu poprawki wprowadzone i całość działa po integracji z głównym projektem.
Ale poziom frustracji wzrósł kiedy dostaliśmy zadanie aby dołożyć w projekcie, a konkretnie w bazie danych, triggery i procedury na bazie danych do wysyłania maili. Bo klient w praktycznie wszystkich systemach wysyła maile przez bazę danych (insert do odpowiedniej tabeli, z której potem jedna usługa to wszystko zbiera). Ale jak to?! Emaile przez SQLa?! Toż to patologia w najczystszej postaci! Dajcie spokój, my Wam to napiszemy od nowa w kodzie, znajdziemy jakąś bibliotekę, pogadamy z działem odpowiedzialnym za udostępnianie takich rzeczy jak np. dane do różnych usług, tydzień pracy i po problemie. Czysto, nowocześnie, po naszemu.
Jednak ostatecznie przepchnęli skorzystanie z bazy i napisanie kilku linijek SQLa. Okropność.
Prosta sprawa (?)
Przejdźmy teraz do projektu głównego, który jest rozwijany głównie przez zespół klienta. Zaczęły pojawiać się zadania związane z wprowadzaniem usprawnień w systemie. Bo aplikacja działa na produkcji i użytkownicy mogą zgłaszać swoje uwagi. Więc jako, że jesteśmy profesjonalistami to wzięli nas do pomocy w uporaniu się z tymi zgłoszeniami.
I jedno z pierwszych zadań, które aktualnie robimy, dotyczy połączenia trzech formularzy w jeden. Po prostu wcześniej użytkownik musiał wchodzić na trzy różne strony żeby wprowadzić zmiany w trzech różnych elementach jednego dokumentu, konkretnie instrukcji dla osób, które potem będą kontrolować ich klientów. W dodatku teraz nie dość, że trzeba edytować na trzech ekranach to jeszcze wprowadzenie zmiany na jednym z nich blokuje możliwość edycji pozostałych dopóki ktoś nie zatwierdzi zmiany.
I tutaj wkracza nasze zadanie. Mamy zamienić te trzy strony w jeden duży formularz. Taki gdzie wprowadzenie zmiany w jednej części faktycznie ją blokuje do dalszej edycji przed zaakceptowaniem zmian ale pozwala modyfikować inne elementy. Na pierwszych spotkaniach dotyczących tego zadania obraz przedstawiał się tak, że jest to głównie zmiana widoków. Ot, połączenie trzech plików HTML w jeden z lekkim odświeżeniem całego wyglądu i zrobienie podobnej operacji łączenia po stronie kontrolerów w backendzie.
Po wielu godzinach dyskusji wymyśliliśmy jak to powinno działać w idealnej sytuacji. Powstała pewna koncepcja, która pozwalała zrealizować ten konkretnie przypadek.
Ale po głębszej analizie kodu okazało się, że pracy jest jednak więcej i frontend nie jest najtrudniejszą z nich. Zwyczajnie wyszło, że obecnie istniejące rozwiązanie było dopasowane tylko do edycji pojedynczych encji. I prawdopodobnie to był powód, dla którego zmiana jednego fragmentu powodowała, że użytkownik musiał czekać na akceptację zanim zmienił inny fragment. Całość jest też połączona z mechanizmem akceptacji zmian, wspólnym dla praktycznie całego systemu.
Ostatecznie wg tego co znaleźliśmy zadanie dotyka frontendu, backendu i bazy danych.
Konflikt wewnętrzny
Nie wiem jak u Ciebie, ale u mnie w sytuacji przedstawionej powyżej przychodzi dosyć szybko na myśl, że najlepiej to przepisać. Wszystko jest źle, nie tak jak chcemy, więc przepiszmy całość, po naszemu, wtedy na pewno będzie prawidłowo. Obecny mechanizm nie wspiera naszej wizji więc tym gorzej dla tego mechanizmu. Dodajmy w tym miejscu nowy kod. Taki, który zrealizuje od początku do końca naszą wizję.
Ale czy na pewno? Czy zamienienie istniejącego kodu nowym spowoduje, że będzie on lepszy?
Zasada dobrego kodu
Jakiś czas temu napisałem tekst o tym po co dbać o jakość kodu. I jedną z zasad tej jakości, zasad dobrego, czystego kodu jest jego spójność. Ta spójność powoduje, że niezależnie od tego jaki fragment projektu otworzymy to poczujemy się jak w domu. Struktura będzie zachowana, rozkład kodu będzie znajomy, logika stojąca za ułożeniem warstw będzie właśnie spójna. Dzięki temu praca z takim kodem jest przyjemna, prosta i sprzyjająca sprawnemu rozwojowi danego systemu.
I teraz wpadamy my, cali na biało. Jednym cięciem wyrzucamy cały moduł i piszemy go od nowa. Wg naszej wizji. Bo nasza wizja jest lepsza, wydajniejsza, nowocześniejsza. Ale przede wszystkim jest nasza właśnie, my jesteśmy autorami i tymi, którzy tchnęli życie w ten zbiór znaków wpisanych w edytorze.
Skoro przepisujemy i piszemy wg swojej wizji to znaczy, że poprzednia koncepcja różni się od naszej. A skoro różni się od naszej to znaczy, że chcemy użyć innej. I teraz co się dzieje kiedy dodajemy inną koncepcję jako fragment innej całości? Zaburzamy spójność! Niszczymy więc jedną z zasad czystego kodu. W takim razie powinniśmy przepisać całość! Tylko w jaki sposób wytłumaczysz, że dodanie zapisywania dokumentów wymaga przepisania też obsługi klientów i wysyłania maili? Części, które ani nie są związane z tym co robisz ani nie są zepsute lub wymagające zmian.
Przepisując funkcjonalność od zera zostajesz więc z brakiem spójności. A ten brak spójności nie będzie już Twoim problemem. Bo Ty wiesz co dopisałeś. Ale kolejna osoba, która będzie utrzymywać taki kod nie będzie wiedziała co pisałeś. Wyobraź sobie, że jesteś na jej miejscu i ktoś każe Ci pracować z kodem, w którym jeden fragment nie pasuje do reszty. A po historii zmian nie masz pojęcia dlaczego do tego doszło skoro wcześniej całość miała większy sens. Ciekawe wyzwanie, prawda?
Kto ma rację?
Skoro mamy tutaj pewnego rodzaju konflikt to warto rozważyć za i przeciw każdej ze stron – zespołu konsultantów, czyli nas i zespołu klienta. Jeżeli chodzi o stronę klienta to mogę bazować tylko na tym czego się domyślam, co jest ogólnie znane albo jak sam bym zareagował będąc po drugiej stronie.
„Nasza racja”
Jeśli chodzi o stronę zespołu konsultantów zatrudnionych do wykonania konkretnych zadań to za i przeciw przepisywaniu wyglądają następująco:
Plusy
- Kod napisany wg nowoczesnego podejścia/schematu i aktualnych dobrych praktyk
- Kod dokładnie dopasowany do aktualnego zadania
- Doskonale wiemy co się w tym kodzie dzieje, nic nas nie zaskoczy
- Nikt nam się nie wcina w pracę, bierzemy całość na warsztat i mówimy „teraz to przepisujemy, nie możesz nic commitować”
Minusy
- Dużo więcej czasu spędzonego na przepisywaniu
- Trzeba przekonać opornego klienta
- Trzeba jakoś postawić granicę między „ich” i „naszym” kodem
- Pewnie klient będzie chciał dokumentację do przepisanego kodu i przynajmniej jedno spotkanie wprowadzające
- Musimy zadbać o dużo testów do nowej wersji, żeby być w miarę pewnymi, że nie zepsuliśmy logiki
Strona klienta
Tak jak wspomniałem tą część piszę opierając się głównie na przypuszczeniach i własnej wizji znalezienia się w takie sytuacji kiedy ktoś chce przepisać jakiś fragment mojego kodu.
Plusy
- Nowoczesny kawałek architektury w moim projekcie
- Kod skrojony pod konkretne zadanie, bez dodatkowych obejść w niedopasowanym module
Minusy
- Tracę spójność w projekcie
- Muszę się nauczyć nowej architektury tylko dla zrozumienia jednego modułu
- Tracę wcześniejszą wiedzę na temat tego fragmentu systemu
- Realizacja zadania przeciąga się nawet kilkukrotnie
- Trzeba od nowa testować wszystko co w jakikolwiek sposób łączyło się z przepisanym modułem
- Muszę nowy kod traktować specjalnie, nie mogę go używać z istniejącymi mechanizmami w moim projekcie bo jego architektura się różni
- Zespół, który go przepisuje musi też poświęcić czas w projekcie na napisanie nowej dokumentacji
- Nie mogę zaangażować w zadanie moich ludzi bo cała wiedza, do czasu zakończenia zadania, jest po stronie konsultantów
Ale dlaczego?
Z powyższej listy wychodzi na to, że wszystkie zalety przepisania kodu widoczne po stronie konsultantów sprowadzają się po stronie klienta do jednego – będę miał nowocześniejszy fragment kodu.
Za to minusy po stronie programistów i klienta w większym stopniu się pokrywają, chociaż odwołują się do dwóch różnych stron. Co gorsza te wspólne punkty odnoszą się do braku pewności czy wszystko będzie działać tak jak wcześniej i podnoszą problem przekazania wiedzy.
Dodatkowo klient traci coś co z jego punktu widzenia będzie bardzo ważne w przyszłości – własne doświadczenie i wiedzę potrzebną do późniejszego utrzymania kodu.
To co my, jako programiści-konsultanci, zyskujemy to jedynie nasza własna wygoda. Zamiast „babrać się” z istniejącym kodem, po prostu piszemy taki, który pasuje do naszej wiedzy i umiejętności.
W dodatku niezależnie od tego czy przepiszemy kod na nowy czy użyjemy istniejącego to musimy zrobić migrację danych, bo jak wspomniałem całość działa już produkcyjnie. Tylko, że migracja danych polegająca na dodaniu nowej kolumny to inna skala problemu niż całkowita zmiana struktury tabel, w dodatku odbiegająca znacząco od obecnej.
Sztuka wpasowania się się w kod
Jako programiście nie pracujemy sami. Nie jesteśmy jedyni i wyjątkowi. Programowanie obecnie to z jednej strony praca w grupie, a z drugiej strony próba wpasowania się w to co już jest. Chęć przepisania kodu albo odrzucanie idei dopasowania się do obecnych standardów są próbą ucieczki od konieczności zrozumienia tego co ktoś napisał.
Programowanie, zwłaszcza komercyjne, to sztuka chodzenia na kompromisy. Prawda zawsze leży pośrodku i deptanie tego co już jest tylko dlatego, że jest inne niż nasza wizja to droga na skróty. Ale też droga, która zazwyczaj kończy się szybko i niekiedy boleśnie. Decydując się na przepisanie tego co już istnieje jasno sygnalizujemy, że odcinamy się od przeszłego kodu, ale też odcinamy się od doświadczeń w nim zawartych. Godzimy się na rozwiązywanie już rozwiązanych problemów. Tak naprawdę dorzucamy tylko do całości kolejny problem. Bo nigdy nie przepiszemy całego kodu na raz. Zawsze przepiszemy jakiś moduł, serwis, fragment dużego systemu. A skoro odcinamy się od tego co było to znaczy, że robimy coś inaczej. Bo w przeciwnym razie po co przepisywać? A skoro robimy inaczej to wprowadzamy element, który nie pasuje do spójnej wizji całości. Całości jako zbioru wszystkich modułów, serwisów, aplikacji, usług zebranych w jeden działający system.
Usprawnienia są nudne
Joel Spolsky, CEO dobrze znanego wśród programistów portalu StackOverflow tak pisze na swoim blogu:
We’re programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We’re not excited by incremental renovation: tinkering, improving, planting flower beds.
I zgadzam się z nim w 100%. Jako programiści zawsze chcemy zaczynać od nowa. Własne konstrukcje to to co nas najbardziej fascynuje i z czego jesteśmy najbardziej dumni. Tak jak w budownictwie czy kinematografii – nikt nie lubi być tym architektem czy reżyserem, który wchodzi w dzieło kogoś innego żeby je dokończyć wg istniejących już reguł.
Nawet w drugim projekcie, który realizuję w pracy dodatkowo sytuacja wygląda podobnie. Mam za zadanie poprawić wydajność istniejącej aplikacji. Brzmi jak super sprawa i prawdziwa gratka dla programisty. Jednak ostatecznie wychodzi na to, że po wciągającej i interesującej analizie problemu powstaje myśl, że fajnie by było to napisać po swojemu. Kod, który bym napisał na pewno działałby lepiej. I rozwiązywałby konkretnie znalezione problemy. A tak to muszę się użerać ze strukturą, którą ktoś już napisał. I czas wypełnia mi nudne szukanie miejsc gdzie mogę zrealizować swoją przełomową wizję naprawy świata za pomocą istniejących funkcji i klas.
Nowy kod to każdy umie pisać
Dopasowanie do sytuacji i znalezienie rozwiązania w istniejącym środowisku to coś, co powinno być głównym celem doświadczonych programistów. Napisać projekt od nowa potrafi nawet stażysta czy junior. W końcu tego uczą wszystkie kursy, bootcampy czy szkolenia. Wpasowanie nowego rozwiązania czy przypadku do tego co już istnieje jest umiejętnością, którą powinniśmy nabywać wraz z doświadczeniem. Bo to, moim zdaniem, pokazuje dojrzałość programisty – umiejętność wciśnięcia swojej idei w ideę innego programisty.
Ale jest też druga umiejętność, niemniej ważna. Jest nią umiejętność pójścia na kompromisy i zrozumienia, że nie zawsze najlepsze rozwiązanie jest tym właściwym. Często, zwłaszcza w biznesie, ważniejsze jest żeby pewne procesy były w miarę standardowe. Dostępne dla każdego projektu, działu, pracownika. Tak jak w przypadku wspomnianego przy okazji API wysyłania maili przez bazę danych. Jest to standardowy proces w tamtej firmie. Mają go opracowanego, przygotowanego, przetestowanego. Jakie mamy argumenty, żeby się od niego odciąć i angażować kilka działów w firmie żeby obsługiwały ten nasz jeden mały fragment kodu na specjalnych zasadach?
Jednak żeby móc wejść w istniejący kod musimy go zrozumieć. Inaczej nie będziemy potrafili się dopasować. Bez zrozumienia jak działa i dlaczego tak działa istniejący kod widzimy tylko jedno rozwiązanie – przepisać. O czym wspomina też Jeff Atwood na blogu Coding Horror:
It’s not that developers want to rewrite everything; it’s that very few developers are smart enough to understand code without rewriting it.
O tym jak trudno jest zrozumieć istniejący kod, zwłaszcza jeżeli jest niższej jakości ,przekonałem się rozpoczynając jedną z serii na YouTubie. Próbuję tam zrobić refactoring istniejącego kodu bez przepisywania całego projektu.
Nie uważam, że mamy się godzić na dożywotnie trzymanie jednego standardu. Zmiany są potrzebne. Ale zwykle powinny być to zmiany stopniowe, przemyślane i uargumentowane. Informujmy więc klienta o zaletach wprowadzania nowych rozwiązań. Pokazujmy ich dobre strony i uczmy jak z nich korzystać. Ale jednocześnie słuchajmy drugiej strony. Bo to jej działalność zaburzamy wrzucając swoje pomysły. I ma ona też swoje racje, na które nie możemy być głusi. Bo pisanie kodu to sztuka kompromisu. Jeżeli uważasz inaczej to czas wyjść z programistycznej piaskownicy.
Dobrze, że o tym tak dużo piszesz. Ważne jest, aby nie tylko nam było wygodnie, ale i innym osobom, które dostaną coś „po nas”. Taka współpraca na pewno jest bardziej owocująca, niż gdyby każdy wrzucał swój schemat myślowy i kazał innym go zrozumieć i jeszcze na nim pracować.
Fajny wystrój.
w plusach i minusach brakuje tych podstawowych:
– minus dla klienta: będzie to dużo więcej kosztować
– plus dla zespołu: więcej zarobimy