Kiedy skupić się na długu, a kiedy na funkcjonalnościach?

Gdy nowy klient pojawia się w firmie, która zajmuje się tworzeniem oprogramowania, oznacza to często zawarcie nowego kontraktu. Umowa zazwyczaj zakłada stworzenie produktu bądź rozwinięcie modułu. W cieniu dostarczanych wartości biznesowych, skradają się również jakość i wymagania jako nierozłączna część umowy. Dodatkowo, produkt musi być dostarczony w określonym terminie. To wszystko sprawia, że rozpoczyna się wielka wojna pomiędzy spełnionymi wymaganiami a czasem.

Aleksander Rydzewski. Lider zespołu w dziale badawczo-rozwojowym Boeing. W swojej karierze był testerem automatycznym oraz programistą fullstack aplikacjach, które ułatwiły pracę wielu ludzi. W aktualnej pracy zajmuje się wyszukiwaniem nowatorskich zastosowań sztucznej inteligencji oraz algorytmów optymalizujących w celu poprawienia usług dostarczanych przez branżę lotniczą. Prywatnie żegluje, jeździ na snowboardzie, interesuje się  technologicznymi nowinkami oraz kulturą azjatycką.


Po określeniu elementów współpracy z klientem, zebrany zespół specjalistów zabiera się do wykonania zadania. Odbiorca produktu po każdej kolejnej wersji, nanosi swoje poprawki. Powaga sytuacji zakłada, że trzeba je wykonać do czasu następnego spotkania, z drugiej strony czas, który zostaje do zakończenia projektu niebezpiecznie się kurczy…

Programiści, zaangażowani w projekt, po pewnym czasie zauważają, że nawet najprostsze zadania zajmują coraz więcej czasu, praca przy rozbudowanym kodzie zaczyna sprawiać problemy a czas, który powinien być przeznaczony na budowanie nowych funkcjonalności, rozciąga się w nieskończoność i nie przynosi rezultatów jakich oczekiwaliśmy. Znaczy to, że do projektu wkradła się trucizna, która powoli uśmierca tworzony produkt i jeśli nie będzie czasu ograniczyć jej rozprzestrzeniania cały projekt legnie w gruzach. Kierownik zespołu wie, że ta trucizna nazywa się długiem technologicznym i dotyka wszystkich projektów informatycznych. Właśnie teraz nadszedł czas na walkę z tą chorobą. Pytanie brzmi kto wygra…

Historia ta dotyka wszystkich projektów. Nawet najlepszy zespół specjalistów z czasem zaczyna odczuwać problem długu technologicznego, który jest zbiorem zadań odłożonych na później, uproszczeń w kodzie oraz błędów tworzonych podczas pracy zespołu. Po pewnym czasie wszyscy programiści zadają sobie pytanie czy można uniknąć powstawania długu technologicznego? Na to oraz inne pytania postaram się odpowiedzieć w tym artykule. Najważniejsze z nich brzmią:

  • Czym jest, na czym polega i jaki wpływ na projekt ma refactoring?
  • Co wpływa na rozprzestrzenianie się długu technologicznego w projekcie?
  • Jakie narzędzia i metody można stosować aby mierzyć dług technologiczny?
  • Jak określić kiedy można pracować nad funkcjonalnościami a kiedy trzeba skupić się na długu?
  • Czy refactoring również jest potrzebny przy pracach nad prototypami?

1. Refactoring

W przedstawionej historii, zauważyliśmy, że dług technologiczny nie występuje od razu. Pojawia się zazwyczaj gdy projekt zwiększa swoje rozmiary, a klient zmienia swoje wymagania co do projektu w kolejnych iteracjach. Dług rozwija się, ponieważ przemyślane na początku struktury, naginane są do warunków wymaganych przez klienta. Nagięcie logiki modułu polega na przekształceniu jej w taki sposób, aby moduł spełniał funkcjonalność jakiej się od niego oczekuje przy jednoczesnym wyrzuceniu najmniejszej możliwej części z aktualnego rozwiązania. Tak stworzona struktura, gdy naginana jest zbyt często, może wprowadzać niepożądany chaos do projektu.

Najprostszą metodą, która pomaga zredukować zagmatwanie kodu to refactoring. Proces ten polega na poprawie konstrukcji kodu w celu uzyskania wyższej jakości produktu. Najczęściej refactoring zmienia kod aplikacji tworząc ją bardziej przejrzystą i zrozumiałą, czasem jednak pozwala dodatkowo wprowadzić wyższą wydajność danej funkcjonalności lub całej aplikacji. Zadanie to jednak nie jest trywialne – przeanalizowanie i dogłębne zrozumienie kodu pochłania wiele godzin. Programista zaangażowany w projekt musi wnikając w wewnętrzne ścieżki systemu, które nie były przeglądane od dłuższego czasu a ich analiza może ujawnić najgorsze koszmary wewnątrz struktury projektowej. Poprawnie przeprowadzony refactoring wpłynie jednak pozytywnie na poprawę czytelności i użyteczności kodu. To z kolei pozwoli na sprawne dodawanie nowych funkcjonalności bez opóźnień wynikających przez chaotyczną i niestabilną konstrukcję.

Aby być w pełni szczerym, pewna grupa programistów uważa, że refactoring dotyczy tylko procesu po zakończonym zadaniu w celu poprawy jakości swojego rozwiązania. Jest to oczywiście część refactoringu, jednakże jest to doraźne uporządkowanie kodu. W tym artykule skupiamy się jednak na procesach, które większa grupa programistów wykonuje chcąc wszczepić nowy kawałek kodu do starego modułu, lub zmieniając strukturę w taki sposób, aby przystosować ją do nowo powstałych wymagań. W skrócie, w artykule skupię się na pracy polegającej na przestawianiu istniejących już klocków tworzonego oprogramowania w celu sprawniejszej możliwości dodawania nowych elementów.

Z perspektywy klienta, który płaci za tworzenie aplikacji, proces refactoringu może nie mieć sensu… Klient nie jest w stanie zauważyć jak jego zmiany wpływają na stabilność i konstrukcje projektu. Niewyjaśniony dostatecznie proces wytwórczy, sprawia że klient nie jest świadom jak drobne zmiany potrafią wpłynąć negatywnie na strukturę niedawno wytworzonego oprogramowania. Niedostateczne doinformowanie o konsekwencjach zmian oraz dodatkowy brak zaufania do firmy, czasem powoduje spięcia, które z kolei przekładają się na jakość oprogramowania. Spięcia te również powodują, że popularność tych technik, np. w małych firmach, często jest zaniedbywana.

Zaniedbywanie technik, które porządkują kod to ewidentny błąd w procesie tworzenia oprogramowania. Chaotyczny kod powoduje, że aplikacja, którą dostarczamy, cechuje się niską jakością i nawet najdrobniejsze zmiany wprowadzane do systemu powodują, że aplikacja jest coraz mniej stabilna. Programiści pracujący przy projekcie, aby unikać skomplikowania i chaosu panującego w systemie często w takim wypadku tworzą obejścia funkcjonalne. Polega to na tym, że specjaliści, którzy nie są świadomi istniejącego rozwiązania powielają istniejące już klasy oraz metody tworząc indywidualne ścieżki dla każdej nowej funkcjonalności. To z kolei powoduje lawinowe obniżenie jakości projektu, które sprawia, że utrzymanie kodu staje się niemożliwe. Tworzony projekt wtedy nie jest dostarczony do klienta lub jest dostarczony w sposób uniemożliwiający jego dalszy rozwój.

Gdy projekt rozrasta się do olbrzymich rozmiarów, klient to tylko jedno ogniwo, które wpływa na pogłębienie długu technologicznego… Praca zespołu w takich wypadkach, ma kluczowe znaczenie na jakość dostarczanego produktu. W przedsięwzięciach tej wielkości występuje błąd ludzki oraz konsekwencje wyboru narzędzi. Dodatkowym czynnikiem może być duża rotacja ludzi, która negatywnie wpływa na jakość produktu. Niezaznajomieni z kodem programiści, często nie znając mechanizmów istniejących w centrum systemu, tworzą kod, który szkodzi aplikacji. Aby zredukować wpływ nieznajomości systemu zmiany powinny być nadzorowane przez bardziej doświadczonych specjalistów. Wybór narzędzi również bywa problemem w projektach długoterminowych. Technologie w świecie nieustannie się zmieniają. Dostrzegamy coraz lepiej dopasowane do zadania narzędzia oraz wady starych rozwiązań. W czasie rozwoju produktu, niektóre rozwiązania przestają być aktualne. W takim wypadku należy zadbać o ich zastąpienie, jeśli nadal to my rozwijamy produkt.

Aspekty przedstawione powyżej sprawiają, że refactoring mimo że w czasie jego wykonywania nie przynosi bezpośrednio wartości biznesowej, pozwala później obniżać koszta wytwarzania oprogramowania. Przy długoterminowych projektach jest to olbrzymia przewaga czasowa. Dodatkowo refactoring wpływa na jakość produktu oraz zadowolenie zespołu, który pracuje z kodem. Niewidocznym efektem ubocznym opisywanego projektu jest obraz naszej firmy jako rzetelnego dostawcy oprogramowania, który dostarcza produkty najwyższej klasy i jakości. To właśnie jakość pomaga utrzymywać dobre znajomości oraz uzyskiwać przyszłe kontrakty. Wrażenie to jest niezwykle ważne, ponieważ pomimo dobrego PRu i marketingu, firma nigdy nie będzie wybierana jako pierwszy wybór, jeśli produkty będą niskiej jakości. Dlatego też uważam, że refactoring oraz dbałość o szczegóły są czynnikami niezwykle istotnymi przy tworzeniu każdego oprogramowania.

2. Redukowanie długu technologicznego

Zaniedbywany dług technologiczny przyjmuje wiele potwornych form, a każda z nich jest równie ciężka do przełknięcia zarówno przez kierownika projektu, jak i klienta, który za to płaci. Aby jednak klient mógł zauważyć na co przeznaczane są jego pieniądze, należy w odpowiedni sposób zaprezentować problemy z tworzeniem nowych funkcjonalności. Przy tej okazji należy również przygotować plan działania, który naprawi istniejący stan rzeczy.

Planując pracę z nigdy nie refactorowanym kodem, w celu osiągnięcia czystego oprogramowania, należy przeanalizować istniejącą już sytuacje. Może się bowiem okazać, że projekt nie jest już do uratowania… Może tak się zdarzyć ponieważ projekt zaniedbywany był przez dłuższy czas i wprowadzenie poprawek dla aktualnej struktury skonsumuje więcej pracy programistów niż było poświęcone do tej pory.

Najczęściej sytuacja tego typu może zdarzyć się, gdy rozwijamy projekt tworzony przez inną firmę, która zaniedbywała pracę nad jakością oprogramowania. Taki stan powinien zostać wykryty jeszcze przed podjęciem współpracy i zakomunikowana klientowi jako podnosząca koszta rozwiązania. Jeśli jednak sytuacja tego typu zdarzyła się w naszej firmie, a klient nie był świadom, że produkt ma zaniedbywane podstawowe praktyki programistyczne, niestety najprawdopodobniej stracimy klienta… Dodatkowo niefortunna decyzja sprawi, że klient na pewno będzie chciał odzyskać jego pieniądze. Nikt nie lubi wyrzucać pieniędzy do kosza…

Jeśli natomiast widzimy możliwość chwilowego, ale świadomego pogłębiania długu w celu zaobserwowania szerszego wachlarza możliwości, to sytuacja ta powinna być przedyskutowana z klientem. Klient wtedy będzie świadom zwiększonego ryzyka, które ponosi decydując się na możliwe zwiększone zyski, ale również negatywne konsekwencje wynikające z tej decyzji, jeśli podjęcie ryzyka okaże się problematyczne. Zaciągnięty dług po tej operacji należy spłacić zmniejszając ryzyko produktu na późniejszych etapach.

To co może wskazywać, że mamy do czynienia z takim systemem to dużo indywidualnych ścieżek dla każdej funkcjonalności, bądź pliki zawierające po kilkaset linii kodu, które są modyfikowane z każdą zmianą. Przedstawione systemy zazwyczaj łatwiej napisać od nowa niż pracować z nieuporządkowanym kodem, który najprawdopodobniej nigdy nie był testowany.

Naprawa takiego stanu może wiązać się z dłuższym czasem, niż choćby połową przeznaczonego na aktualne rozwiązanie. Innym problemem, z którym trzeba zmierzyć się w czasie doraźnej redukcji długu to przestarzała technologia. Może zdarzyć się, że kluczowe dla działania systemu biblioteki nie są już wspierane i mają zbyt dużo problemów wewnętrznych, aby dalej z nich korzystać.

W 2018 roku 3 naukowców z norweskiej politechniki w Oslo przeprowadziło badanie [1] na grupie 226 pracowników z różnych szczebli kariery w 15 dużych firmach. W skład tej grupy wchodziły zarówno osoby, które biorą udział w konstruowaniu kodu, ale również architekci oraz osoby, które zajmują się kierowaniem projektów. Dodatkowo trzy firmy, które bardziej dbały o kod zostały przepytane o dokładne techniki, które wykorzystują, aby monitorować i zapobiegać powstawaniu długu technologicznego. Niewątpliwie interesującą rzeczą jest to że aż około 73% badanych osób nie monitoruje długu technologicznego i nie ma czasu przeznaczonego na jego poprawę.

Co ciekawe inne badanie[2] z 2008 roku pokazuje, że około 68% projektów informatycznych nie jest dostarczonych, bądź przekracza 200% swojego budżetu. Osoby zaangażowane w te projekty uważają, że oprócz niesprecyzowanych wymagań, często błędem są złe narzędzia monitorowania stanu projektu oraz błędne zarządzanie ryzykiem w projekcie. Brzmi znajomo prawda?

Norweskie badanie z 2018 roku sugeruje, że około 27% wszystkich respondentów wplata czas przeznaczony na poprawki jakościowe i zredukowanie długu technologicznego w proces tworzenia oprogramowania. Osoby, które brały udział w takim badaniu stwierdziły że czas, który jest przeznaczony na takie poprawki wydłuża czas tworzenia nowych funkcjonalności między 25 a 40%. Można więc założyć, że średnio redukowanie długu wydłuża realizację projektu o około 1/3 czasu. Jednakże są projekty, w których czas ten jest dłuższy niż 40% (nawet do 90%) – zazwyczaj są to projekty polegające na utrzymaniu produktu w dobrej jakości przez bardzo długi czas. Wplatanie poprawki jakościowej w proces tworzenia aplikacji jest sensownym podejściem, które pozwala zredukować dług technologiczny. Nie jest to jednak idealne rozwiązanie o czym powiem później.

Testy

Świadomy wykonywanej pracy inżynier, powinien mieć czas na dogłębne zrozumienie struktury z jaką pracuje i możliwość wykonania zmiany w takim stopniu, aby nie uszkodzić struktury kodu. Aby wykonywać cięcia z chirurgiczną precyzją, potrzebne są jednak testy. Pokrycie kodu testami pozwala nie tylko poprawiać złożone funkcjonalności bez obawy o ich uszkodzenie (co przyspiesza pracę programisty w późniejszych terminach), ale również pokazuje dla klienta realizację założeń niefunkcjonalnych i odporność systemu na błędy użytkowników. Aby testy były wykonywane regularnie należy wzbogacić system kontroli wersji (np. GIT) o systemy Continuous Integration (np. Jenkins, TeamCity), które z każdą zmianą dostarczaną do wersji produkcyjnej testują rozwiązanie przy pomocy testów jednostkowych i wydajnościowych.

Dobrą metodyką tworzenia testów jest również TDD (Test Driven Development), które polega na stworzeniu testów walidujących nieistniejącą funkcjonalność, a następnie zaimplementowaniu funkcjonalności, która sprawi, że poszczególne testy wykonają się poprawnie. Podobną technikę stosują testerzy manualni w swoich testach tworząc na początku scenariusz, który chcą zrealizować. Testy wykonywane są wtedy bardziej świadomie i nie są naginane do uzyskanego rozwiązania.

Nie bądź małostkowy przy code review

Inną znaną i popularną techniką jest „Code Review”, który polega na umożliwieniu programistom przeglądania dostarczanego kodu za pomocą wbudowanych funkcjonalności w systemy GitHub lub GitLab. Do tego celu również zostały stworzone dedykowane aplikacje, takie jak np. FishEye. Specjaliści zaangażowani w projekt powinni mieć czas, by przejrzeć kod wyprodukowany z kolegą z zespołu i zadecydować o zmianach, które powinny być zastosowane przed dopuszczeniem kodu do produkcji. Moim zdaniem jest to świetna technika wpływająca dodatnio na projekt, ponieważ nad pojedynczą częścią nie pracuje jeden programista, a cała grupa patrząca na kod przez pryzmat swoich umiejętności, zapewniając mu przejrzystość oraz wysoką wydajność.

Dodatkowo technika ta pozwala uczyć nowych pracowników w projekcie funkcjonalności zaszytych w systemie, bądź wydajniejszych technik dostępnych w technologii. Przy Code Review czai się również nieznaczna pułapka – należy pamiętać, że jest to praca nad projektem i nie można patrzeć na kod przez pryzmat przyjaźni. Kilka razy spotkałem się z pracą, która musiała być odrzucona i podzielona na mniejsze kawałki, ponieważ liczba zmian w projekcie była zbyt duża jak na code review dla jednego człowieka. Mimo przyjaźni i pracy poświęconej przez te osoby, jakość projektu była ważniejsza niż czas spędzony nad poprawkami. Z drugiej strony przy code review nie należy być małostkowym. Jeśli osoba po raz dziesiąty przegląda nasz kod i czepia się do źle pociętego kodu to traci ona czas projektowy powodując opóźnienia. Taka sytuacja powinna być przedyskutowana w zespole.  

Na pewno obecność testów, code review i czas wpleciony w proces tworzenia oprogramowania pomagają w redukcji długu technologicznego. W wypadku gdy możemy zająć się refactoringiem podczas naszej pracy, należy pamiętać o problemie, z którym przyszliśmy do danej sekcji aplikacji. Zdarza się, że poprawki tak nas pochłoną, że zapomnimy o tym jaki problem chcieliśmy zrealizować na początku. Wpleciony refactoring ma dodatkowo tę wadę, że nawet jeśli pracownik poświęca 80% czasu na dług technologiczny, kierownik projektu nie jest tego świadom. To prowadzi nas do kolejnego problemu jakim jest monitorowanie wielkości długu technologicznego.

3. Monitorowanie wielkości długu technologicznego

W poprzednim punkcie przestawiłem, ile czasu konsumuje praca z długiem technologicznym. Jednakże czas ten jest szacunkową wersją wydarzeń prezentowaną przez programistów i architektów, którzy nigdy nie monitorowali czasu poświęcanego na walkę z długiem technologicznym. Zespoły zapytane przez norweskich naukowców o to, jak monitorują ilość pozostałego długu technologicznego odpowiedziały, że często zapisują takie informacje w dokumencie Excel bądź Word (Naprawdę?! Jestem trochę zażenowany…), który jest przechowywany w intranecie firmy. To rozwiązanie nie jest zalecane przez swoje wady, którymi są możliwość zgubienia pliku, ciężka dostępność i to, że najprawdopodobniej nikt do niego nie zagląda poza sfrustrowanymi programistami.

Inni respondenci odpowiedzieli, że w dużej liczbie projektów występują komentarze TODO, w miejscach gdzie należy przeprowadzić refactoring. Aktualne narzędzia pracy programistów takie jak Eclipse, VisualStudio, InteliJ czy WebStorm potrafią monitorować takie miejsca, jednakże nie jest to również technika zalecana – może być jednak techniką wspomagającą. Jest to spowodowane tym, że ciężko określić co sugeruje kod TODO. Może to być mały blok kodu, ale równie dobrze może implikować poprawki na kilku plikach wchodzących w skład aplikacji.

Inna bardziej doświadczona część respondentów odpowiedziała, że do śledzenia problemów związanymi z długiem technologicznym używana jest specjalna tablica w systemie do śledzenia zadań takich jak JIRA, Redmine, PivotalTracker bądź Bugzilla. Norwescy badacze stwierdzili, że jest to najlepsza możliwa forma śledzenia długu technologicznego, ponieważ tablica jest skonstruowana oddzielnie w stosunku do realizowanych zadań, a zarazem jeśli programista poświęca czas na uporządkowanie kodu to dokładnie wiemy, ile czasu poświęcił na to zadanie. Programiści, którzy napotykają problemy w kodzie mogą bez problemu tworzyć zadania, które rozwiązują mniej palące kwestie oprogramowania albo wskazują zauważone zaszłości systemu. Zadania te jednak nie powinny wyglądać jak narzekanie czy lament na temat złej struktury kodu.

Jeśli osoba z zespołu widzi problem do rozwiązania powinien mieć czas na opisanie go, zasugerowanie jak mocno i jak często spowalnia to pracę jego, jego kolegów bądź systemu pod względem wydajności i spróbować oszacować ile czasu powinno zająć te zadanie. Tablica taka nie powinna być również traktowana przez kierowników projektów jako śmietnik czy ujście frustracji programistów. Każdy problem znajdujący się na tej liście samoczynnie nie zniknie. Każde zadanie z tej listy również powinno być zrewidowane przez architekta aplikacji, bądź osobę najbardziej zaznajomioną z modułem. Osoba odpowiedzialna za weryfikację zgłoszonego zadania powinna przemyśleć jego sens, połączyć zadania, które odpowiadają za ten sam element oraz poprawić estmaty.

Następnie jeśli zadanie nie odpowiada za jakość kodu, a jest poprawką wydajnościową  aplikacji, powinno być skierowane do właściciela bądź kierownika produktu. Następnie w każdej iteracji tworzenia oprogramowania powinien być przeznaczony czas do poprawek jakościowych zgłoszonych na takiej tablicy. Jest to bardzo wygodna metoda redukowania długu technologicznego – przynajmniej do czasu wyjścia nowocześniejszej metody tworzenia aplikacji nad jaką pracujemy.

Statystyczna analiza kodu

Inną metodą walki z długiem technologicznym są testy i statyczna analiza kodu. Statyczna analiza kodu polega na przejrzeniu struktury aplikacji przez IDE i określenie miejsc, w których funkcje się powtarzają, warunki pętli, które nie mają szansy się wykonać oraz funkcje czy zmienne, które nigdy nie są wykorzystywane w naszej aplikacji. Taka forma testów pozwala nam na trzymanie kodu w dobrej kondycji i nie zaśmiecanie kodu niepotrzebnymi elementami, które tylko zaciemniają postrzeganie kodu. Według norweskich badań najczęstszą technologią obok IDE do analizy statycznej jest SonarQube. Drugą wspomnianą metodą mierzenia wzrastającego długu technologicznego są testy.

Metryka pokazująca liczbę błędów wykrywanych przez testy, testerów oraz na produkcji również może być w tym momencie pomocna. Jeśli zauważymy, że w jakimś module liczba błędów narasta, a nowe funkcjonalności nie są stworzone z zachowaniem najwyższej jakości, może oznaczać to, że dany moduł może mieć problemy ze stworzoną architekturą, którą należałoby zrewidować i poprawić w celu zmniejszenia powiązań między funkcjonalnościami. Jeśli nie będziemy zwracać uwagi na narastającą ilość błędów, może okazać się, że nigdy nie skończymy naszego projektu ponieważ powiązania i błędy przerosną wartość dodaną do produktu.

Techniki monitorujące dług technologiczny

Dodatkową techniką podnoszącą jakość przy produkcji skomplikowanych systemów może być audyt jakościowy. Co pewien okres, jeden sprint powinien być przeznaczony na poprawienie jakości i zaobserwowanie miejsc, gdzie jest problem z architekturą. Audyt tego typu stosowany w firmie Uber został zaprezentowany w książce Susan J. Fowler [3] i sprawdza się doskonale w dużych projektach, które są utrzymywane przez długi czas przez wiele osób. Osoby te podczas specjalnego spotkania z architektem proszone są o zaprezentowanie tego, jak rozumieją aktualną strukturę aplikacji/modułu oraz jej działanie.

Wytłumaczenie funkcjonalności modułu w uporządkowany sposób zajmuje z reguły sporo czasu i odbywa się na indywidualnym spotkaniu po to, aby zmniejszyć stres pracownika. Pozwala jednak na zaobserwowanie, części systemu, którego programiści nie rozumieją, gdzie pojawiają się obejścia oraz jak postępuje narastanie długu technologicznego. Zadanie to należy do sprawnego architekta, który zauważy te problemy i stworzy zadania polegające na uproszczeniu konstrukcji, bądź jej zoptymalizowaniu. Reszta czasu przeznaczonego na audyt powinna być przeznaczona na rozwiązanie tego typu kwestii. To  cykliczne wydarzenie ma nie tylko dodatni wpływ na jakość produktu, ale również na zespół, który cyklicznie może komunikować problemy ze zrozumieniem produktu lub przedstawić swoje wątpliwości co do niejasnej struktury w kodzie.

Podsumowując techniki monitorujące, należy zaznaczyć, że zarówno audyt, stworzenie tablicy z błędami, jak i testy sprawdzają jak mocno nasza konstrukcja stała się niestabilna. Pozwalają więc na analizę wielkości istniejącego już długu technologicznego. Aby zobaczyć przyszłość i ewentualne pogłębienie długu należy zatrudnić sprawnego architekta, który analizuje aktualną sytuację na rynku technologii. Niestety żadne narzędzie nie jest w stanie planować za nas architektury, dlatego lider technologiczny powinien posiadać czas na przeanalizowanie nowo pojawiających się opcji. Analiza ta powinna również zawierać w sobie czas na zaplanowanie prac, które zmienią nasz produkt w taki, który używa najnowszych zdobyczy techniki.

Jeśli sytuacja zostanie źle przeanalizowana zostaniemy dinozaurem przegonionym przez konkurencję szybciej adaptującą się do rynku. Dodatkowo należy nadmienić, iż żadna metoda nie pozwala na wyeliminowanie długu technologicznego całkowicie. Metody te jedynie pozwalają redukować dług do utrzymywalnego poziomu. Błędy ludzkie, nieustanny rozwój oraz zmiany technologii nie pozwalają nam na pozostawienie systemu samemu sobie. Skończyłoby się to na pewno źle.

4. Prototypy

Prototyp to początkowa wersja produktu, która ma pokazać wartość biznesową pomysłu. Do pracy nad prototypem z reguły przeznaczona jest mała grupa ludzi rozwijających oprogramowanie na małą skalę (stąd nazwa MVP – Minimum Viable Product). Dopiero gdy prototyp pozyska pewno grono zainteresowanych klientów prototyp przekształcany jest w pełnoprawny produkt. Dodatkowo do projektu dostarczeni są dodatkowi ludzie, którzy mają ułatwić przynoszenie dodatkowej wartości biznesowej w szybszy sposób. Tego typu podejście jest opisane również w książce Dan’a Olsena[4]. Porównuje on to do zespołu, który rzuca małymi kamieniami w różnych kierunkach nasłuchując czy kamień trafi w coś ciekawego. Jeśli kamień w pewnym kierunku wyda interesujący nas dźwięk wytaczamy ciężkie działa które sprawią że osiągniemy satysfakcjonujący nas efekt.

Mimo tworzenia nowych funkcjonalności należy pamiętać, że MVP może być również produktem, którego mimo pierwotnych analiz klienci nie oczekują. Dlatego w pierwszej kolejności należy ustalić jakie wymagania funkcjonalne i niefunkcjonalne ma spełniać produkt. Pierwsza wersja aplikacji, bardzo często znacząco różni się od tego, czego oczekiwać będą prawdziwi użytkownicy systemu. Często klient, który płaci za produkt nie zdaje sobie sprawy z tego, czego użytkownicy naprawdę chcą. Dlatego w pierwszych wersjach systemu należy skupić się na tworzeniu kodu w taki sposób, aby był on możliwie najłatwiej przystosowany do zmian.

Zmiany przy prototypach występują dość często przez operacje zwane „pivotem”, czyli zmianą pozwalającą na lepsze dopasowanie się firmy do rynku. Jest to naturalne zachowanie w świecie startupów. W takim momencie często z systemu wycinane są najlepsze elementy albo odpowiednie algorytmy. Zdarza się również że prototyp, który tworzymy, ląduje w koszu i prace zaczynamy od początku. Jest to sytuacja podobna do przerośniętego długu technologicznego – okazuje się, że czas poświęcony na poprawki przekroczyłby czas tworzenia nowego systemu. Przyznanie się do tego błędu jest niewątpliwie ciężkie, ale zaprezentowane w odpowiednim momencie potrafi zaoszczędzić dużo pieniędzy dla firmy klienta.

Dług architekta

Należy jednak pamiętać, że nie można stworzyć prototypu i wprowadzić go na produkcję. Zazwyczaj prototyp powstaje innymi technikami i na mniejszą skalę, niż prawdziwy produkt. Z tego też powodu posiada dług technologiczny, którego trzeba się pozbyć przed wdrożeniem na wielką skalę. Dług ten jednak jest rozmyślnym posunięciem strategicznym. Dobrze skalowalne rozwiązanie często znacznie przekracza czas przeznaczony na stworzenie całego prototypu. Zastosowanie rozwiązania na taką skalę jest kompletnie bezużyteczne, gdy nie jesteśmy pewni wartości biznesowej produktu, a efekty testujemy na małej grupie użytkowników w testach walidacji biznesowej. W takim wypadku inteligentny architekt świadomie zaciąga dług, który w przyszłości ma zamiar spłacić.

Niedorzecznym wydaje się tworzenie aplikacji na cały rynek światowy zainteresowany np. kulturystyką, ponieważ może okazać się, że nasze założenia będą kompletnie chybione. Z drugiej strony tworzenie małego prototypu i wdrażanie go w pełną skalę produkcyjną, gdzie ludzie mogą przekazywać sobie do niej dostęp również jest mało rozsądne. Ilość użytkowników powinna być ustanowiona z klientem na początku, jak również inne wymagania niefunkcjonalne. Klient może nie być świadom, że niektóre wymagania są zbyt wygórowane, dlatego jako specjaliści mamy za zadanie rzetelnie poinformować klienta o zaistniałej sytuacji i przygotować model bezpiecznego wdrożenia systemu do rynku.

System, który był projektowany pod mniej restrykcyjne wymagania z reguły zablokuje dostęp lub sprawi, że cała struktura stanie się niestabilna i pozostawi użytkowników z dużą dozą niezadowolenia. Ludzie z reguły są niecierpliwymi istotami. Jeśli technologia, którą im dostarczyliśmy będzie się zacinać i będzie jedynie niedużo lepsza od konkurencji to najprawdopodobniej użytkownicy wrócą do starych nawyków i aplikacji, które działają natychmiastowo. Odzyskanie ich zaufania może potrwać bardzo długo, co sprawi że najważniejsze początkowe zyski nie będą wystarczające, aby przetrwać i zachwycić użytkowników.

Należy założyć, że prototyp jest małą wersją większej aplikacji – należy zredukować wymagania funkcjonalne i niefunkcjonalne do takiej grupy, która pozwoli nam na stworzenie świetnej aplikacji działającej w małym zakresie. Zaciągnięcie takiego długu jest zazwyczaj świadomą decyzją architekta i powinna dotyczyć ona jedynie momentu przed wzbudzeniem zainteresowania użytkowników.

Podsumowując. Kiedy skupić się na długu a kiedy na funkcjonalnościach?

Dług technologiczny jest podstępnym efektem ubocznym tworzenia i utrzymywania oprogramowania. W każdym projekcie, w którym ma on miejsce, programiści muszą stosować refactoring mimo braku bezpośredniego wpływu na wartość biznesową po to, aby rozwijać projekt w terminie z zachowaniem wymagań niefunkcjonalnych. Aby na początku szybko walidować nasze idee możemy świadomie zaciągać dług technologiczny wykorzystując pewne uproszczenia algorytmów, które sprawią, że nasz produkt będzie szybciej zwalidowany biznesowo. Musimy jednak pamiętać o tym, aby w dłuższej perspektywie spłacić dług, pracując nad jakością stworzonego rozwiązania, przed oficjalnym wdrożeniem systemu.

W dłuższej perspektywie współpracy z klientem należy jednak wplatać refactoring w czas naszej pracy wykorzystując podstawowe techniki tj. Code Review oraz pokrycie aplikacji testami. Aby jednak nie przepalać czasu w sposób, który irytuje klienta i móc zaprezentować klientowi wyniki nad jakimi pracujemy, należy monitorować status naszego projektu. Aby to robić efektywnie do tej pory zaprezentowałem stworzenie tablicy do śledzenia problemów zgłaszanych przez programistów czy audyt jakościowy pokazujący zrozumienie struktury aplikacji przez programistów. W skład tej grupy metod wchodzi też statyczna analiza kodu, która pozwala na uproszczenie konstrukcji kodu i stworzenie bardziej zrozumiałej struktury.

Jeśli potrzebujemy twardych liczb, aby przekonać klienta do racji wykonywania refactoringu przedstawiam kilka ostatnich metod policzalnych pomiarów w oparciu o wyżej wymienione techniki:

  1. Metryka ile zadań wróciło z testów z powrotem do programistów i ile czasu zajęło rozwiązanie poprawek. Metryka ta pokazuje skomplikowanie modułu systemu, który jest niezrozumiały dla programisty. Każda „zwrotka” do programisty kosztuje klienta czas testera, ponieważ będzie musiał sprawdzić on te zadanie podwójnie. Jeśli zadań jest dużo czas przeznaczony na testowanie również jest drogi. Dodatkowo każda godzina pracy programisty również jest kosztem. Jeśli koszt przepalony nad poprawkami wprowadzonymi do zadania jest większy niż dostarczany kod funkcjonalności musimy zainterweniować.
  2. Nowe błędy vs. Zamknięte – Pokazuje nam poziom „zatrucia” aplikacji chaosem. Jeśli suma zamkniętych problemów w danym okresie jest mniejsza niż poziom otwartych na nowo, a dodatkowo wszystkie dotyczą funkcjonalności jednego modułu – może to sugerować coraz większy chaos w danej części aplikacji.  Refactor w tym momencie zawsze jest potrzebny.
  3. Zmiany w starym kodzie – Ta metryka jest tworzona manualnie, bądź za pomocą skryptu. Należy sprawdzić jak często dany plik był modyfikowany w danym okresie czasu (w tym zadaniu pomagają funkcjonalności systemu GIT). Jeśli plik zawiera się w nowej funkcjonalności, może być nieustannie zmieniany i nie jest to błąd. Jeśli jednak znajdziemy plik, który istnieje w systemie od dłuższego czasu i aktualnie jest on dość często zmieniany może sugerować to że aktualna wersja systemu nie jest przystosowana do poprzedniej logiki zawartej w pliku. Należy uelastycznić strukturę kodu tak, aby zmiany w danej sekcji kodu nie były potrzebne w dłuższej perspektywie.
  4. Testy – testy powinny pokrywać więcej niż 75% kodu. Dużo współczesnych IDE ma możliwość sprawdzenia jak duża część kodu jest pokryta testami. Brak testów również jest długiem technologicznym ponieważ nie możemy monitorować błędów pojawiających się podczas zmian. Do poprawy natychmiastowo.
  5. Czas reakcji naszej aplikacji na zachowania klienta – Jak już pisałem wcześniej – ludzie to dość niecierpliwe istoty. Jeśli będziemy kazać im czekać dłużej niż 2-3 sekundy, wybiorą inne technologie rozwiązujące ich problemy.

Dbanie o jakość dostarczanych aplikacji jest najważniejszym zadaniem zespołów projektowych. Refactoring pozwala trzymać systemy przy życiu mimo braku bezpośredniego wpływu na wartość biznesową projektu. Pośrednio jednak proces ten jest potrzebny tak jak tlen istotom żywym do oddychania. Bez refactoringu i uporządkowania, entropia tworzonych zmian i poprawek rozdarła by nasz kod, a my spędzalibyśmy olbrzymią ilość czasu na analizowaniu zagmatwanej logiki aplikacji.

Jeśli jednak kogoś nie przekonują takie metryki, a jedynie bezpośrednia wartość pieniężna wynikająca z kosztów operacyjnych, to również znalazłem argument czysto matematyczny. W 2014 roku została wydana książka[5], która między innymi opisuje pod kątem finansowym, kiedy należy wprowadzić refactoring do projektu. Na podstawie tej książki stworzyłem cztery wzory, które pomagają to ustalić.

Każda iteracja posiada swój koszt utrzymania aktualnego kodu. W przypadku książki jest to aż rok, ale uważam, że średni koszt kwartału również się sprawdza. Aby wyliczyć koszt iteracji należy policzyć, ile kosztują zmiany oraz błędy powstałe w aplikacji za pomocą prostego wzoru (1). Błędy i zmiany są to wszystkie zmiany, które powstają w procesie tworzenia oprogramowania oraz u klienta.

Drugim krokiem jest obliczenie średniego kosztu pracy nad refactoringiem w zespole za pomocą wzoru (2).

Następnie trzeba obliczyć współczynnik przyrostu kosztów. Aby to zrobić trzeba skorzystać ze wzoru (3) dodając do siebie koszty utrzymania w ostatnich N kwartałach(wyliczone wzorem (1)), następnie podzielić tę wartość przez koszt pierwszego kwartału biorącego udziału w pomiarze, który jest pomnożony przez N. Następnie odjąć wartość, która wyjdzie od wartości 1. Otrzymamy wtedy znormalizowany % przyrostu kosztów utrzymania.

Zazwyczaj do tego równania używa się 2 lub 3 ostatnich kwartałów, aby jednak otrzymać mniej zaburzony wynik możemy dodać więcej miesięcy do równania. Dużo osób zatrzymuje się na tym kroku i wylicza czy przyrost kosztów nie wynosił więcej niż 5%. Jeśli ta wartość miary wynosi więcej korzysta z refactoringu. Są różne uzasadnienia tej decyzji. Jedne teorie mówią, że taki jest średni zysk jakościowy z refactoringu z czym się nie zgadzam, ponieważ nie bierze np. pod uwagę późniejszej opinii o naszej firmie jako twórcy oprogramowania. Inna dość kuriozalna teoria brzmi, że jeśli zebralibyśmy cały koszt utrzymania aplikacji w danym kwartale do banku na lokatę otrzymalibyśmy większy zysk.

Jeśli jednak chcemy dla klienta oszacować dokładną miarę zysku należy skorzystać ze wzoru (4) – tzn. dodać do siebie wynik ze wzoru (1) dla ostatniej iteracji i (2), a następnie odjąć od niego koszt z ostatniej iteracji pomnożony przez współczynnik. Zysk powinien wyjść nam w walucie, którą wyliczyliśmy pozostałe wartości.

Mimo że koszt jest jak najbardziej poprawny i może przekonać wiele osób, osobiście uważam, że pomija wiele ważnych cech, takich jak np. ryzyko w danym momencie utrzymania projektu. Może się okazać, że kod jest jest bardzo zagmatwany, więc koszt utrzymania w momencie rozpoczęcia pomiaru już był bardzo duży. W takim wypadku w kolejnych miesiącach koszt nie będzie się zwiększał tak jak powinien, ponieważ i tak będzie on bardzo zawyżony i będzie sprawiał dużo kłopotów programistom. Nawet duże błędy popełnione przez programistów nie będą aż tak bardzo zmieniać tego współczynniku  

Inną sprawą jest fakt, że przy wysokim ryzyku uzyskanym podczas tworzenia oprogramowania może zdarzyć się, że refactoring nie może być już zastosowany ponieważ przewyższa jakiekolwiek koszta produkcji oprogramowania. Wyliczone estymaty również nie zawierają informacji o tym, ile programiści konsumują czasu zajmując się wszczepianiem swoich rozwiązań w istniejący już system, co bardzo często jest głównym kosztem tworzenia oprogramowania. Sądzę więc, że te estymaty pasują bardziej tylko dla czysto utrzymaniowych zespołów z niedużym długiem technologicznym co zdarza się niezbyt często.

Refactoring jest jedną z istotniejszych rzeczy, które sprawiają, że produkt potrafi dojść do fazy wydania jej dla właściwych użytkowników. Zachęcam do monitorowania długu technologicznego przez tak proste i potrzebne dla produktu metryki jak testy, liczba zadań która wróciła do fazy produkcyjnej czy czas odpowiedzi. Jeśli pewna metryka zaczyna sprawiać kłopoty przeprowadźmy mały audyt, stwórzmy tablicę do śledzenia problemów i zlokalizujmy błędy z naszym zespołem. Sprawi to że niskim kosztem oszczędzimy sobie wielu godzin pracy w celu wprowadzenia zmian do skomplikowanego i nie testowalnego kodu. Zaoszczędzony czas w przyszłości będziemy mogli wykorzystać na tworzenie szybciej tak cennej dla nas wartości biznesowej.


Zdjęcie główne artykułu pochodzi z unsplash.com.

Bibliografia:

[1] A. Martini, T. Besker, J. Bosch, “Technical Debt tracking: Current state of practice: A survey and multiple case study in 15 large organizations”, Science of Computer Programming, Elsevier, 163 (1), pp. 42–61, October 2018.

[2] J. Verner, J. Sampson and N. Cerpa, “What factors lead to software project failure?” 2008 Second International Conference on Research Challenges in Information Science, Marrakech, 2008, pp. 71-80. doi: 10.1109/RCIS.2008.4632095

[3] S. Fowler, “Production-Ready Microservices”, O’Reilly Media, 1 edition (December 15, 2016)

[4] D. Olsen “The Lean Product Playbook: How to Innovate with Minimum Viable Products and Rapid Customer Feedback”, Wiley, 1 edition (June 2, 2015)

[5] I. Mistrik, R. Bahsoon, R. Kazman, Y. Zhang “Economics-Driven Software Architecture” Morgan Kaufmann; 1 edition (July 2, 2014)

Patronujemy

 
 
Polecamy
Python nie taki straszny. Zobacz, co warto wiedzieć na początku drogi