Dalej rozwijać aplikację, czy lepiej ją przepisać? O legacy code

W każdej firmie IT znajduje się aplikacja, którą można określić mianem legacy. Istnieje duża szansa, że takim określeniem spotkaliście się w pracy, w literaturze lub podczas rozmów z kolegami po fachu. Definiuję ją jako aplikację działającą od pewnego czasu na produkcji (w moim odczuciu mówimy o latach, jednak to zależy od dynamiki projektu, technologii), przy której pracował zmienny zespół programistów oraz najczęściej przechodziła pewne modernizacje technologiczne mające na celu uaktualnić samą aplikację. Wszystkie z wcześniej wymienionych przeze mnie cech powodują narastanie długu technologicznego. Czym jest dług? To abstrakcyjna wartość, którą można opisać jako zwiększenie trudności utrzymania, rozwoju, jakości oprogramowania.


Szymon Szczepański. Software Architect and Team Leader at HP Software Polska. Pracuje w branży 11 lat przechodząc wszystkie kolejne szczeble podczas pracy dla DXC Technology (wcześniej HPE a jeszcze wcześniej HP). Zaczynał jako junior programmist obecnie piastuje pozycje architekta systemów IT w projektach o zasięgu ogólnopolskim w sektorze public. Na co dzień utrzymuje system składający się z kilkunastu aplikacji i integrujący się z wieloma systemami zewnętrznymi. Stara się wprowadzać najnowsze rozwiązania do istniejącego stosu technologicznego i dbać o najwyższe standardy kodu aplikacji. Jest współprowadzącym zajęć z Programowania w Środowisku Graficznym na Politechnice Warszawskie dla wydziału MiNI.

Co go zwiększa?

1. Błędy analizy biznesowej wychodzące w późnym etapie pracy nad zmianą lub na produkcji.

2. Wprowadzanie nieprzemyślanych zmian w systemie.

3. Wybranie „drogi na skróty” w przypadku konieczności wprowadzenia większych zmian w kodzie.

4. Ciągłe zmiany zespołu wprowadzającego zmiany.

5. Dodawanie nowych technologii do istniejącego oprogramowania.

Konsekwencjami długu najczęściej jest:

1. Wydłużony czas wprowadzania zmian.

2. Większa liczba defektów w aplikacji.

3. Dłuższa analiza statyczna kodu.

4. Trudniejsze wdrożenie nowych członków zespołu.

Zaniechanie przeciwdziałaniu długu technologicznego może doprowadzić do tego, że nasza aplikacja będzie napisana zgodnie ze wzorcem spaghetti. Oczywiście możemy zminimalizować efekt oddziaływania długu technologicznego przeciwdziałając przytoczonym przeze mnie punktom. Mimo wszystko nie da się całkowicie wyeliminować tego procesu i w końcu staniemy przed dylematem: czy dalej rozwijać aplikację, czy może ją „przepisać”. I w większości o podjęciu tej decyzji będzie ten artykuł.

Podjęcie decyzji

W poprzednim akapicie skończyliśmy na sparafrazowanym hamletowskim: „przepisywać czy nie? Oto jest pytanie”.  Jak stwierdzić, czy to ten moment? Może refactoring wystarczy? Czy naprawdę tego potrzebujemy? Tak naprawdę nie istnieje jasna odpowiedź na te pytania, niemniej postaram się przedstawić kilka opinii.

Zacznijmy od tego, że proces wytwórczy oprogramowania jest procesem złożonym. Zaangażowane w niego są różne grupy, m.in.: programiści, testerzy, analitycy, architekci i inni. Każda z nich będzie dochodziła do postanowionego na początku problemu w innym czasie. I choć grup jest wiele, skupię się na programistach-projektantach.

Zacznijmy nasze rozważania od ostatniego pytania, czyli „czy może wystarczy refactoring?”. Jestem zwolennikiem refactoringu, a nawet może zbyt chętnie go stosuje. Co do zasady refactoring robimy tylko na zmienianym przez nas kawałku kodu. Celem tej zmiany jest poprawienie czytelności, funkcjonalności kodu lub dostosowanie go do np. SOLID. Ważne, aby rozdzielić refactoring od przepisania. Przesłanką do tego drugiego mogą być:

1. Refactoring dotknąłby z 30-40% aplikacji — z punktu widzenia zarówno technicznego, jak i biznesowego tak, dlatego idące zmiany wymagają dużo czasu, są zazwyczaj bardzo skomplikowane, a co za tym idzie mogą generować dużą ilość incydentów. Trzeba też pamiętać, aby taki refactoring nie wprowadził większego długu technologicznego w innych modułach przez co może prowadzić do problemów.

2. Ograniczenie technologiczne – np. zmiana systemu operacyjnego lub zmiana frameworku, przejście z systemu „on-premises” na chmurę, zmiana sposobu integracji z klientami czy też konieczność zmiany zamierzchłego modułu np. do obróbki grafiki. Podczas próby dostosowania aplikacji może okazać się, że koszt dostosowania istniejącej aplikacji jest bardzo wysoki, albo że nie istnieją biblioteki w wersji na przyjęty framework lub też, że zmiany są typu breaking changes i dostosowanie aplikacji do nich może być ciężkie. Czasem sprawa jest jasna i przechodzimy np. do chmury, a co za tym idzie różni się to tak diametralnie, że bez zmiany dużej części aplikacji nie da rady nawet uruchomić aplikacji.

3. Wydajność aplikacji z każdym releasem spada – jeżeli nasza aplikacja działa z każdym releasem wolniej, a np. zapytania SQL-ów są coraz bardziej skomplikowane i ich wykonanie jest coraz dłuższe, ilość zwracanych danych jest mocno nadmiarowa, ale wynika z wprowadzonego modelu nawet po refactoringu. Oznacza to, że trzeba przemyśleć, czy nie popełniliśmy gdzieś błędów w projektowaniu aplikacji i czy nie trzeba by jej przeprojektować.

4. Wiele punktów sterowania w metodzie, czyli kolokwialnie mówiąc: „if-ologia stosowana” – taka sytuacja najczęściej prowadzi do stworzenia ścieżek, które nie były przewidziane przez analityków/architektów i mogą prowadzić do defektów aplikacji lub też co gorsza do ukrytych i nieprzewidzianych zmian danych. Dodatkowo duża ilość punktów sterowania niesie konieczność rozbudowy procesów testów oraz utrudnia analizę kodu. Często też tworzą się martwe strefy, czyli miejsca gdzie praktycznie nigdy aplikacja nie powinna wejść, a jednak zawierające pewną logikę. Przy małej ilości często refactoring może pomóc, jeżeli nasz kod wygląda jakby jego fundamentem była instrukcja „if” możemy dojść do punktu 1 próbując dostosować aplikację.

5. Duże, widoczne jedynie z poziomu kodu, zależności pomiędzy modułami biznesowo niepołączonymi – jeżeli zostaną takie rzeczy zidentyfikowane to zazwyczaj znaczy, że granice pomiędzy dwoma Bounded Contexty (terminologia z DDD) zatarły się. Jeżeli takie rzeczy mają miejsce znaczy to, że skomplikowanie naszego kodu jest zbyt duże, jego czytelność zbyt mała, albo zmianę przeprowadzał developer nie mający styczności z biznesem stojącym za rozwiązaniem. Z mojego doświadczenia możemy tu zauważyć efekt kuli śnieżnej, raz przełamanie granic skutkuje coraz większym mieszaniem się elementów dwóch Bounded Contextów aż do momentu, gdzie ich znaczenie domenowe jest zatarte.

6. Metody, których długość znaczenie przekracza przyjętą przez nas długość metody – najczęściej jest to spowodowane dopisywaniem nowych funkcjonalności w nowo dodanym if-ie z tłumaczeniem: „bo nie chciałbym czegoś zepsuć”. W konsekwencji dostajemy sytuację z punktu 4, często również z dodatkowym smaczkiem, czyli poprawieniem kodu przy błędzie w jednym miejscu, a nie we wszystkich miejscach, w których biznesowo to powinno być zrobione. Związane jest to z kopiowaniem bloków kodu wewnątrz metody, a następnie z poprawą tylko części, która jest w problematycznej ścieżce, bez przeanalizowania reszty kodu. Refactoring czegoś takiego jest bardzo utrudniony, gdyż brak jest przesłanek czy zrobione to zostało celowo czy też jest to błąd. Niestety przez to czas, który trzeba poświęcić na poprawienie jakości kodu jest dużo dłuższy oraz istnieje duże prawdopodobieństwo reakcji kaskadowej, czyli powstania błędów na innych nieprzewidzianych ścieżkach.

7. Brak możliwości jasnego wytłumaczenia wprowadzonych zmian w kodzie podczas Code Review – może być to przesłanka, że osoby zrobiły zmianę na zasadzie prób i błędów. Konsekwencje takich modyfikacji często są duże np.: niedziałające inne ścieżki zależne od tego kodu, działanie tylko dla konkretnego przypadku, wprowadzanie do kodu kolejnych instrukcji sterujących dla każdego kolejnego przypadku, brak zrozumienia biznesowego błędu. Sytuacja taka pokazuje nam, że kod najprawdopodobniej jest skomplikowany, nieczytelny lub osoba ma zbyt małe doświadczenie i nie powinna bez nadzoru zmieniać kodu. Doświadczony programista lub architekt powinien przejrzeć dany fragment aplikacji i zweryfikować czy faktycznie jest on dobrze napisany czy może wprowadzać w błąd.

8. Przekazywanie wiedzy jest mocno utrudnione – jeżeli osoba, która ma przekazać wiedzę ma problem ze zrobieniem tego, bo sama gubi się w kodzie, czy też używa słów typu: hack, obejście, myk, sztuczka, magia itp. Znaczy to, że kod jest za skomplikowany i wprowadzanie w nim zmian będzie bardzo trudne. Może okazać się, że pewne niedoskonałości zostaną ukryte w danym module, a potem przy próbie jego rozwoju będą dawać o sobie znać.

Chciałbym zaznaczyć, że pojedynczy punkt jest zaledwie przesłanką, jednak jeżeli 4-5 punktów wystąpi w jednym czasie to już poważny argument do zmiany. Jeżeli w jednym czasie będzie 6-8 przesłanek to już, moim zdaniem powód, żeby wstępnie przyjąć potrzebę przepisania aplikacji.

W poprzednim akapicie starałem się przekazać przesłanki, kiedy nasza aplikacja jest w stanie kwalifikującym ją do przepisania. Teraz musimy sobie zadać sobie pytanie: „Czy na pewno tego potrzebujemy?” Żeby odpowiedzieć na to pytanie moim zdaniem musimy się zastanowić nad następującymi punktami:

1. Jak często zmieniamy aplikację – jeżeli zmieniamy ją raz na dwa lata, to koszt przepisania może być za wysoki. Jeżeli jednak jest ona ciągle modyfikowana, zmiana może być opłacalna.

2. Jak krytyczna jest dla nas ta aplikacja – krytyczne aplikacje są zawsze na widoku, zależy nam na tym, aby nie miały błędów, działały szybko i stabilnie. Ta aplikacja jest poniekąd naszą reklamą i wiadomo, że lepiej, żeby reklama była dobra.

3. Jak dużo innych aplikacji komunikuje się z tą aplikacją – czym więcej, tym wdrożenie będzie cięższe, bo jest więcej punktów, które trzeba dokładnie przetestować.

4. Czy aplikacja jest wewnętrzna czy zewnętrzna – zazwyczaj z zewnętrznymi aplikacjami jest więcej problemów natury formalno-prawnej o czym też należy pamiętać.

5. Co prócz powyższych zyskamy na przepisaniu aplikacji – może rozszerzymy kompetencje zespołu, a może wprowadzimy rzeczy, które zawsze były traktowane jako wymagające zbyt dużej ilości zmian, może uprościmy procesy biznesowe.

6. Czy mamy, jak przetestować nowe rozwiązanie – jeżeli posiadamy pewien zakres testów integracyjnych, testów UI, testów systemowych to można je dostosować i użyć. Jeżeli nie mamy gotowych scenariuszy testowych, przetestowanie nowego rozwiązania może być problematyczne.

7. Czy mamy kompetencje w zespole – jeżeli przepisywać to zazwyczaj budujemy ją na stosie technologicznym, który różni się od oryginalnego. Warto sprawdzić jaki mamy poziom kompetencji w zespole.

8. Czy posiadamy łatwo dostępną wiedzę biznesową – jeżeli mamy ludzi, którzy znają biznes od podszewki to łatwiej będzie nam dobrze zrobić system niż w przypadku posiadania jedynie dokumentów do starszej wersji systemu.

9. Czy czas implementacji zmian jest dłuższy w perspektywie czasu – jeżeli tak może to oznaczać, że w zamian za zostawienie aplikacji będziemy częściej płacić mniejsze koszty.

10. Czy ilość defektów rośnie z każdym releasem – podobnie jak wyżej, wprowadzenie zmiany w mocno skomplikowanym systemie z dużą ilością zależności mogą pojawiać się błędy wynikające z reakcji łańcuchowych między powiązaniami. Aby mieć pewność, że nasze oprogramowanie jest dobrej jakości na produkcji należy zwiększyć nakład na testy aplikacji legacy.

Ważne też, żeby zdawać sobie sprawę z następujących faktów:

  • Przepisanie dla samego przepisania nie zadziała – jest takie proste przysłowie „lepsze jest wrogiem dobrego” i dokładnie o to chodzi w tym punkcie. Każde przepisanie jest to złożony proces generujący duży narzut sił zespołów, więc jeżeli nie mamy powodu nie przepisujmy czegoś, bo możemy to zrobić lepiej.
  • Zawsze pojawią się problemy z przyjętą technologią – nie zdarzyło mi się jeszcze, że nowa technologia, którą wybrałem niczym mnie nie zaskoczyła. Niektóre rozwiązania, inny sposób działania niż zakładany, ograniczenia których nie wzięło się pod uwagę, niekompatybilność z jakimiś elementami systemu to są tylko niektóre rzeczy mogące spędzić sen z powiek. Do tego, jeżeli technologia jest nowa dla wszystkich to potem na produkcji na pewno jeszcze nie jedna rzecz nas zaskoczy.
  • UI zawsze nie będzie się do końca podobać i znajdą się ludzie, którzy powiedzą, że stare było lepsze – kwestia przepisywania UI zawsze jest bardziej skomplikowana. Dochodzi tu aspekt tego jak jest odbierane przez osoby trzecie, a tu oceny zawsze będą się rozkładać jak krzywa Gaussa z tym, że przesunięta albo w stronę : „jest super” albo „jest źle”.
  • Zmiana modelu danych zawsze jest ciężka – jest to związane z przemigrowaniem (przemapowaniem) danych z jednego modelu do drugiego, gdzie często okaże się, że historycznie brakuje nam jakiś danych albo pojawią się artefakty, które będą wymagały indywidualnego traktowania. Testowanie migracji danych również jest bardzo skomplikowane.
  • Jest bezpośrednia zależność między wielkością a trudnością przepisania, przewagę mają tu mikroserwisy – Microservice ze swojej definicji powinny być łatwo przepisywalne, dzięki mocnej specjalizacji. Zazwyczaj przez to ich rozmiar nie jest za duży, a skomplikowanie biznesowe sprowadza się do algorytmów biznesowych które są wydzielone z całego procesu.
  • TDD jest dobre – dzięki TDD będziemy na pewnym poziomie wstanie sprawdzić czy zachowania naszej aplikacji odpowiadają tym z poprzedniej aplikacji. Oczywiście nie wszystkie testy jednostkowe nam to umożliwią, ale pewne zaadaptowane w odpowiedni sposób mogą nam ułatwić cały proces.
  • Będzie moment utrzymania dwóch systemów – najczęściej przełączenie między systemami, szczególnie ważnymi biznesowo, nie odbywa się w trybie jednorazowego przełączenia. Często występuje pilotaż rozwiązanie czy inna forma stabilizacji rozwiązania, gdzie użytkownicy końcowi mogą zgłaszać błędy. Trzeba pamiętać, że przez ten okres w poprzedniej wersji oprogramowania również mogą pojawiać się błędy i te należy sprawdzić na obu wersjach aplikacji, aby potem nie wróciły ponownie do nas
  • Programista chętniej dołączy do zespołu, jeżeli technologia stosowana będzie w miarę aktualna – duża liczba programistów mi znanych lubi brać udział w projektach, które używają nowych technologii, tego co jest aktualnie modne w środowisku i co ma powodować łatwiejszy i szybszy sposób tworzenia aplikacji. Jeżeli nasz stos technologiczny projektu jest w miarę nowy programista od razu spojrzy przychylnym okiem na naszą ofertę.
  • Rynek wyznaczy trendy, którymi chcąc nie chcąc trzeba podążać, aby nie zostać w tyle – czasami na wizytach u klientach można spotkać się z wyrażeniami: „Słyszałem, że teraz wszystko robi się na kontenerach… czemu u nas nie jest to w kontenerach? Chcemy mieć nowoczesne oprogramowanie”. Jeżeli będziemy aktualizować nasz stos technologiczny a aplikacje będą dobrze w nim napisane to łatwiej będzie dyskutować z klientem o tym, że to co robimy jest innowacyjne i dobre.

Reasumując, załóżmy, na potrzeby artykułu, że przeważają argumenty za przepisaniem aplikacji, zarówno technicznie jak i biznesowo. Pozostała odpowiedź na ostatnie pytanie: czy to odpowiedni moment? Moim zdaniem lepiej zacząć wcześniej nawet mniejszymi siłami. Jednak trzeba wziąć pod uwagę, że przepisanie aplikacji w zależności od wielkości może trwać od tygodni po miesiące. Jak widać jest to czasochłonny proces szczególnie z powodu konieczności utrzymania dwóch systemów. Nie ma sensu również zaczynać procesu, jeżeli w perspektywie mamy duże zmiany, które musimy wprowadzić przed szacowanym zakończeniem prac. Moim zdaniem również nie ma co przenosić procesów 1:1, można poświęcić ten czas, aby trochę zoptymalizować. Warto przemyśleć też zmianę podejścia do procesów, choć tu można się spotkać z niechęcią użytkowników końcowych.


Zdjęcie główne artykułu znalezione na medium.com.

Patronujemy

 
 
Polecamy
13 rozwiązań podnoszących bezpieczeństwo aplikacji w Node.js