Utrzymywanie aplikacji to też programowanie
Jak rozwijać istniejący system: od odtworzenia błędu, przez bezpieczny refaktoring, po wdrożenie i obserwację.
Utrzymywanie aplikacji to też programowanie
Nowy projekt zaczyna się od pomysłów. Istniejąca aplikacja zaczyna się od ograniczeń: danych, integracji, przyzwyczajeń użytkowników i decyzji, których autorów czasem nie ma już w zespole. To nie czyni pracy nad nią mniej techniczną. Często wymaga ona większej ostrożności i lepszego rozumienia konsekwencji zmian.
Dobre utrzymanie nie polega wyłącznie na reagowaniu na zgłoszenia. Chodzi o to, żeby aplikacja działała przewidywalnie, a kolejna zmiana była łatwiejsza do wykonania.
Najpierw odtwórz problem
Wyobraźmy sobie endpoint obliczający wartość zamówienia. Użytkownik zgłasza, że dla części zamówień rabat nie jest naliczany. Można od razu poprawić warunek w kodzie. Wcześniej warto jednak odpowiedzieć na trzy pytania: jakie dane wywołują problem, jaki wynik powinien powstać i skąd wiadomo, że właśnie taki wynik jest poprawny?
Źródłem oczekiwanego zachowania może być kontrakt API, uzgodnienie biznesowe albo istniejący test. Sam aktualny kod nie wystarczy: może przecież zawierać błąd.
Minimalny przypadek odtwarzający zgłoszenie bywa bardziej wartościowy niż długa lista hipotez. Pozwala oddzielić problem z regułą rabatową od problemu z deserializacją, konfiguracją lub danymi w bazie.
Oddziel zmianę zachowania od porządkowania
Załóżmy, że cena, rabat i zapis zamówienia są obsługiwane w jednej dużej metodzie. Podczas naprawy łatwo ulec pokusie przebudowania całej klasy. Wtedy recenzent musi jednocześnie ocenić poprawkę i nową strukturę kodu.
Wygodniej pracować małymi krokami:
- Dodać test odtwarzający niepoprawne zachowanie.
- Wprowadzić możliwie małą poprawkę.
- Potwierdzić wynik także dla przypadków granicznych.
- Osobno wydzielić odpowiedzialności, jeśli uprości to kolejne zmiany.
Test charakteryzujący istniejący kod pomaga opisać jego dzisiejsze zachowanie. Nie oznacza jednak, że każde zastane zachowanie należy zachować na zawsze. Jeśli utrwala błąd, oczekiwanie trzeba świadomie zmienić.
Kod powinien ujawniać decyzje
Poniższy fragment jest przykładem struktury, a nie kompletną implementacją zamówień:
BigDecimal subtotal = calculateSubtotal(order);
BigDecimal discount = discountPolicy.calculate(order, subtotal);
BigDecimal total = subtotal.subtract(discount);
validateTotal(total);
saveOrder(order, total);
Widać tu kolejność czynności i miejsca, w których podejmowane są decyzje. Żeby poznać reguły rabatowe, nadal trzeba zajrzeć do implementacji. Nie trzeba natomiast najpierw rozplątywać zapisu do bazy, formatowania odpowiedzi i obliczeń.
Samo dzielenie kodu na metody nie daje tej korzyści. Nazwy typu processData() i handleResult() ukrywają sens równie skutecznie jak długa metoda. Ważne jest nazwanie odpowiedzialności.
Utrzymanie obejmuje to, czego nie widać w kodzie
Przy zgłoszeniu „aplikacja czasem nie działa” przydają się metryki, logi i możliwość powiązania wywołań między serwisami. Log powinien opisywać zdarzenie i kontekst potrzebny do diagnozy. Kopiowanie całego żądania może zwiększać hałas i ujawniać dane użytkownika.
Spring Boot Actuator udostępnia integrację z metrykami, m.in. dla żądań HTTP. To punkt wyjścia do obserwacji, nie gotowa odpowiedź na przyczynę awarii. Dokumentacja metryk opisuje dostępne mechanizmy.
Równie ważne są odtwarzalny build, opis konfiguracji i regularne aktualizacje zależności. Aktualizacja wykonywana w małych odstępach zwykle ma mniejszy zakres do przeanalizowania niż skok przez kilka generacji bibliotek. Jej ryzyko nadal zależy od tego, co faktycznie się zmieniło.
Poprawka kończy się po wdrożeniu
Przejście testów jest potrzebne, ale nie pokazuje całego środowiska produkcyjnego. Przed wdrożeniem warto wiedzieć, jak rozpoznać poprawę, jaki sygnał oznacza regresję i jak wycofać zmianę. Przy migracji danych samo uruchomienie poprzedniej wersji aplikacji może nie wystarczyć — zgodność schematu trzeba zaplanować wcześniej.
Dla poprawki rabatu takim sygnałem może być poprawny wynik wskazanego przypadku oraz brak wzrostu błędów na endpointach zamówień. Dla zmiany wydajnościowej potrzebny będzie pomiar pod porównywalnym obciążeniem.
Najbardziej użyteczna poprawka usuwa problem i pozostawia po sobie wiedzę: test, czytelniejszy fragment kodu albo krótkie wyjaśnienie decyzji. Dzięki temu następna osoba zaczyna pracę od lepszego punktu.