When it is worth to rewrite a project?

27 july 2023
Jakub Rojek Jakub Rojek
Photo by Sigmund on Unsplash (https://unsplash.com/photos/aI4RJ--Mw4I)
Categories: IT analysis, Programming, Methodologies

Projekty informatyczne rodzą się i rozwijają w różny sposób. Niektóre są zaplanowane od początku do końca i rzeczywiście ten plan jest cały czas realizowany. W zdecydowanie większej liczbie przypadków zespół spotyka różne zakręty po drodze, do których można zaliczyć np. nadmiernie zbliżającą się datę wykonania, wymagania zmienione "w locie" czy brak uwzględnienia określonego przypadku przy planowaniu architektury. Jednak w takich sytuacjach zazwyczaj pomaga regularna refaktoryzacja, która wyprostuje kod w spokojniejszym czasie. Niestety, dobrze wszyscy wiemy, że bywają też projekty, w których różne zdarzenia doprowadzają do tego, że po pewnym czasie programiści coraz śmielej mówią o napisaniu projektu od nowa. Równie śmiało kierownik projektu mówi wówczas "ale skąd".

W jaki sposób można poznać, że oprogramowanie rzeczywiście wymaga napisania go od nowa, a kiedy jest to tylko fanaberia i frustracja programistów? Czy są w ogóle takie sytuacje, kiedy po wielu latach kod rzeczywiście nadaje się do zaprojektowania i realizacji od początku? Na pierwsze pytanie będziemy starać się odpowiedzieć niżej, natomiast na drugie możemy odpowiedzieć od razu - tak, jak najbardziej istnieją takie przypadki. Tym niemniej, tylko mały procent z nich rzeczywiście doczeka się ponownej realizacji i - jak się pewnie domyślacie - przyczyny leżą w biznesie.

Co sprawia, że projekt wymaga restartu?

Wiele osób na to pytanie zapewnie odpowie, że wystarczającym argumentem jest brzydki kod, pełen przykrych zapachów. Jednak nie jest to prawdą - nie każdy paskudny kod nadaje się do przepisania całego oprogramowania od nowa. A przynajmniej nie w warunkach biznesowych, które koniecznie trzeba uwzględnić w swoich planach i jakie najtrudniej wytłumaczyć tzw. technologicznych pistoletom, wołający jako pierwsi o restart projektu, bo to po prostu frajda. I tak, przyznaję - tworzenie architektury i rozpoczynanie czegoś od nowa, z nadzieją na lepszą przyszłość, to rzeczywiście frajda, ale niekoniecznie właściwe rozwiązanie.

Po pierwsze, należy zidentyfikować, gdzie są tak naprawdę problemy z kodem. Na tym etapie często okazuje się, że dotyczą one tylko konkretnych fragmentów oprogramowania lub ujawniają się w określonych momentach. W takim przypadku nie ma sensu przepisywać wszystkiego - znacznie łatwiej jest wydzielić konkretną część do osobnego modułu lub nawet podaplikacji i ją refaktoryzować. To jednak też trzeba robić z wyczuciem, ale o tym później. Sytuacja zmienia się, gdy po analizie okazuje się, że problem z kodem leży u samych fundamentów, np. w obsłudze kluczowych obiektów zarządzanych przez system. W takim układzie o drobne zmiany tu i ówdzie jest znacznie trudniej.

Po drugie, należy przejrzeć stos technologiczny używany w oprogramowaniu. Najczęstszą realną przyczyną przepisywania systemów na nowo jest historyczny kod, bazujący na wersjach bibliotek, które są archaiczne lub po prostu już niewspierane. Przykładem może być aplikacja, która powstała w Angularze 2 i od tego czasu nie była aktualizowana (w chwili, gdy piszę te słowa, dostępna jest wersja 16) - w takim wypadku nie ma się co dziwić, że programiści narzekają na rozwój oprogramowania w tak starej technologii. Warto też wówczas rozważyć, czy aktualizacja wersji rzeczywiście jest problematyczna - może nie będzie tak źle i wystarczy tylko dostosować parę rzeczy. Choć trzeba przyznać, że czasem to droga przez mękę, szczególnie w frameworkach, w których twórcy wprost odrzucają tzw. kompatybilność wsteczną (ang. backward compatibility).

Po trzecie, należy wziąć pod uwagę ostatnie zmiany i kierunek nowych wymagań klienta. Czy faktycznie sprawiały one, że to, co w "normalnej" architekturze zrobilibyśmy w kilka dni, tutaj potrwało kilka tygodni i wygenerowało dużo nieznanych wcześniej problemów? Przy czym pytanie dotyczy nie tego, czy dało się to zrobić szybciej, tylko czy powodem opóźnienia były problemy z dotychczasowym kodem. Jeśli tak i sytuacja się powtarza, to rzeczywiście staje się to solidnym argumentem do przepisania dużej części systemu. Natomiast warto przeanalizować również, czy zmiany zgłaszane przez klienta faktycznie są sensowne i konieczne do realizacji - być może te same cele biznesowe można osiągnąć w inny sposób lub wręcz obecny kierunek przeczy wstępnym założeniom. Są to jednak problemy do rozstrzygnięcia na szczeblu biznesowym i zarządczym, a czasem oznaczają wręcz drugą aplikację.

I wreszcie, po czwarte, trzeba to wszystko skonfrontować ze środowiskiem biznesowym. Jeśli mimo swoich problemów rozwój projektu jest konieczny i klient osiąga dzięki niemu korzyści, z których software house też korzysta, to nagłe powiedzenie "stop" i rozpoczęcie prac od nowa nie jest najlepszym wyjściem. To walka kompromisów - z jednej strony nie można zaprzestać modyfikacji, a z drugiej brnięcie w zły kod tylko pogłębia problem i dług technologiczny. Żeby jeszcze tego było mało, to nie ma za bardzo możliwości, aby łączyć rozwój i restart projektu ze sobą - pierwsze musi się zatrzymać, aby drugie mogło efektywnie trwać. Na szczęście, istnieje sposób, aby to przyspieszyć.

Jak przeprowadzać restart i większą refaktoryzację?

Specjalnie użyłem terminu "większa refaktoryzacja", gdyż mimo sprzyjających okoliczności nadal realizacja projektu od nowa nie zawsze jest dobrym pomysłem. Powiem więcej - zwykle jest niewykonalna w takiej czystej formie, chyba że zespół ma dużo środków i czasu, a także nie musi nigdzie się spieszyć (np. jest liderem rynku w danym obszarze). Dużo popularniejsze podejście mówi o tym, aby transformować obecne oprogramowanie stopniowo, krok po kroku. Jednak do tego trzeba się przygotować.

Przede wszystkim przydatna jest analiza rzeczywistych problemów, którą programiści tworzą przed podjęciem decyzji. Nie ma projektów, w których wszystko jest złe - są takie, w których pewne obszary są tak źle zrobione, że wpływają na inne, potencjalnie "normalne". Być może w aplikacji do zarządzania stajnią problemem nie jest obsługa koni czy budynku, ale system uprawnień użytkowników, przez który każdy nowy ekran oznacza dwukrotną pracę? A może przyjęto złe założenia dotyczące wprowadzenia API, dobierając przy tym nieodpowiednią technologię? Warto jeszcze raz przejrzeć raporty czasów spędzonych nad danymi wymaganiami i znaleźć takie, które notorycznie przynoszą opóźnienia. Taka analiza jest potrzebna do tego, aby zdecydować, co rzeczywiście będzie zmieniane.

Drugim etapem jest podjęcie decyzji i zaplanowanie kierunku zmian, a więc przemyślenie zmian architektonicznych oraz wydzielenie części, która zostanie objęta zmianą. W tym miejscu niektórzy mogą powiedzieć, że to dobry moment na zaktualizowanie frameworka i bibliotek, ale osobiście radziłbym się przez chwilę z tym wstrzymać i zauważyć, że przecież podejście może być zupełnie inne. Może technologia, która została użyta w projekcie, nie jest w ogóle wspierana lub wręcz nie pozwoli na realne zmiany? Niektórzy z nas sami zetknęli się z podobnymi sytuacjami, np. wiele lat temu, gdy standardowa kombinacja Java + GWT stosowana przy panelach administratora okazywała się coraz większym ciężarem i to z kilku powodów - zamiast tego uprościliśmy podejście i zaczęliśmy wykorzystywać mechanizmy dostarczane najpierw przez Yii 1.0, a później Yii 2.0, co znacząco przyspieszyło pracę, ale też pozwoliło większej grupie osób w przyszłości pracować przy takich projektach. Jeśli jednak uznamy, że obecne technologie są dobre, tylko należy je zaktualizować, to faktycznie jest to dobry moment - na tym etapie nie są jeszcze realizowane żadne zmiany architektoniczne, a jedynie dostosowanie kodu do nowej wersji bibliotek. Chyba że to samo w sobie oznacza solidną modyfikacją użytych struktur.

Trzecim etapem jest ustalenie kolejności przeprowadzania zmian - fatalnym pomysłem jest robienie wszystkiego naraz (chyba że faktycznie mówimy o zupełnie nowym projekcie). Trzeba do tego podejść z rozsądkiem - jeśli pewne kwestie mogą być zmieniane równolegle i nie wpływają na siebie, to można się o to pokusić, jednak warto przemyśleć, czy nie jest możliwe wydzielenie pewnego modułu, który zostanie przepisany od nowa. A potem kolejnego i tak dalej. Tak jest - tutaj błyszczą podejścia modułowe i mikroserwisowe, gdyż wydzielają one poszczególne obszary oprogramowania i ułatwiają zmiany. Czasem staje się to naturalne, a czasem trzeba się trochę postarać. Niewątpliwą zaletą jest to, że naturalnie taki podział zostaje i w przyszłości zaowocuje łatwiejszym planowaniem prac w danych częściach.

W szczególności warto rozważyć wydzielenie modułów, które będą dostępne dla reszty oprogramowania przez API (a nie bezpośrednio w kodzie jak do tej pory), a następnie napisanie obok nowego modułu, który w przyszłości zastąpi ten stary. Zaletą jest to, że wówczas wystarczy po prostu przepiąć starą część na nową. W ten sposób w ostateczności przez jakiś czas obie wersje mogą działać koło siebie i być porównywane pod kątem różnych kryteriów. Oczywiście, trudno to rozważać w momencie, kiedy problemy z aplikacją są fundamentalne i dotyczą elementów, których nie da się wydzielić. Ale przyznam szczerze, że to dość rzadka sytuacja.

Inną zaletą podejścia krok po kroku jest też fakt, że po drodze może się okazać, iż pełna zmiana (czy też refaktoryzacja)... nie jest potrzebna. Być może po przepisaniu 30% oprogramowania okaże się, że system działa już jak należy i najważniejsze problemy zostały rozwiązane? Oczywiście, do tego potrzebna jest solidna analiza, o której pisałem wyżej - to warunek, bez którego nowy kod nie będzie znacząco lepszy od starego, a przynajmniej taki efekt będzie bardziej kwestią szczęścia niż rozumu. Jednak gdy zrobi się wszystko "uczciwie" - jest szansa, że nie wszystko zostanie "zresetowane", a zespół i klient będą mieli możliwość stopniowego rozwoju oprogramowania przy ewentualnie równoległych dalszych zmianach w architekturze.

Warto wspomnieć o jednej rzeczy - należy zadbać o to, aby pisanie nowej części oprogramowania nie wprowadzało nowych błędów. W takim układzie warto przemyśleć kwestię testów automatycznych lub przynajmniej innego podejścia weryfikującego poprawność transformacji.

Podsumowanie

Oczywiście, czasem nie ma siły i konieczne jest przepisanie oprogramowania na nowo. Tym niemniej, w wielu sytuacjach solidna i dobrze przeanalizowana refaktoryzacja staje się lepszym wyjściem. Być może jest też tak, że problem jest tylko np. z frontendem, a nie backendem i można przepisać tylko połowę aplikacji? Są to decyzje zależne od konkretnego projektu, jednak należy pamiętać, że każde podejście do oprogramowania od nowa jest dużym przedsięwzięciem, które wymaga środków i zapasów czasowych. Dlatego - jak mówi klasyk - lepiej zapobiegać niż leczyć i starać się tworzyć dobre jakościowo oprogramowanie od początku. Niekoniecznie idealne, ale wystarczająco dobre (ang. good enough), aby zadowolić i harmonogram, i budżet, i rozwój.

Pozdrawiam i dziękuję - Jakub Rojek.

We can do quite a bit and what is more, our skills and resources are at your disposal. Take a peek at what we can offer you.

About author

Jakub Rojek

Lead programmer and co-owner of Wilda Software, with many years of experience in software creation and development, but also in writing texts for various blogs. A trained analyst and IT systems architect. At the same time he is a graduate of Poznan University of Technology and occasionally teaches at this university. In his free time, he enjoys playing video games (mainly card games), reading books, watching american football and e-sport, discovering heavier music, and pointing out other people's language mistakes.

Jakub Rojek