Csináljunk legacyból modern szoftvert, egy Angular frontend projekt élményei

« All posts

(Csináljunk legacyból modern szoftvert, egy Angular frontend projekt élményei)

Csatlakoztam egy frontend projekthez, amiről már az első perc előtt tudtam, hogy alacsony kódminőségű (a híre ugyanis megelőzte), és bár aktív fejlesztés alatt van, legacy érzésű a vele való munka. Az új feature-ök fejlesztéséhez a meglévő rossz/elavult mintákat használják, semmi indítattás nem mutatkozott a dolgok jobbá tételére. A tétlenség fluktuációt eredményezett a fejlesztők körében (az évek alatt 50+ ember dolgozott a kódon), a fluktuáció pedig ignoránssá tette őket, így egy önmagába roskadó spirált generálva.

A projektet egy olyan szoftvercég emberei rakták össze eredetileg, amelyik arról híres, hogy a minőséget inkább mennyiséggel próbálja kompenzálni, sikeretelenül. A megbízók azt hitték, hogy a unit test code coverage magas százelékú előírása minőségi kódot eredményezhet - természetesen tévedtek, a "tesztek" gyakorlatilag akadályai voltak a szoftver jobbá tételének később, és nem voltak többek szimpla függvényhívásoknál, valódi ellenőrzés (assertion), lényegében tesztelés nélkül.

Angular/TypeScript projekt, tehát lett volna lehetőség a kvázi-statikus típusosság kihasználásának, mégis a legtöbb helyen figyelmen kívül volt hagyva, remek táptalajt szolgáltatva az olyan hibáknak, amik csak a fejlesztői környezeten kívül fordulnak elő és közvetlenül a végfelhasználók szeme előtt jelentkeznek. Ha voltak is típusok definiálva, gyakori volt az azonos nevű és célú, viszont teljesen különböző helyen és különböző módon implementálva (mindenhol épp csak annyi, amennyi a típusellenőrzőt elcsitítja, semmi több).

A stabilitást további futásidejű (feature flag, autentikáció, cookie consent, stb) és fejlesztési idejű (nyelvi fordítások, ikonpakkok) külső függőségekkel veszélyeztetjük: 3rd party szolgáltatók, amik rendelkezésre állása nem kiszámítható, sem számonkérhető, de az is maximum vihar utáni kármentés, a bukás minden alkalommal a megbízó nevéhez fűződik.

Miután a projekt backendje is nagyon sok függőséggel rendelkezik, amik néhol egymásra is támaszkodnak, integrálódnak, a teljes képet megszerezni sok idő volt (még most sincs meg teljesen).

A gondolat, hogy ezzel valamit kezdeni kell, már azelőtt megszületett, hogy a projekthez csatlakoztam. "Ezt újra kellene írni" - gondoltam és nyitottam meg először a kódot. De elég ideje vagyok a szakmában, hogy tudjam, hogy egy ekkora méretű és ilyen szerepű projektet, ekkora mérnöki kitettséggel nem lehet csak úgy újraírni, ki kell találni rá egy stratégiát. Ezen a stratégián gondolkodtam a megismerés ideje alatt, majd elkezdtem összeszedni azokat az ötleteket, amik szerintem a jó sínre tehetik - életében először - a projektet.

A problémák

Az egyik megfigyelésem, ahogy fent említettem, a típusosság drasztikus hiánya. Hibák komplett kategóriáját ki lehet szűrni azzal, ha minden változónak rendes típust adunk meg. Ez tehát az egyik pont a tervben.

A kód struktúrája és formája teljesen inkonzisztens. Nincs két egyformán kinéző fájl (kivéve az a 2000 soros, amit kompletten átmásoltak, hogy csak egy függvénynevet változtassanak meg benne). A különbözőségek lassú és rossz olvashatóságot eredményeznek, komoly hatással vannak a bővíthetőségre és lehetőséget adnak hibák elkövetésére.

A projekt architektúrája abszolút esetleges. Többféle szerepkör (néha 6-8 is) egyetlen komponensbe került mindenféle racionális ok nélkül (például: a fejlécben volt a "harang" ikon, ezért a fejlécben volt implementálva a komplett értesítéskezelés a-tól z-ig).

A komponensek egymagukban komplett modulokat valósítottak meg, óriási mennyiségű üzleti logika, megjelenési komplexitás, state kezelés egyben. A unitok (amiket aztán tesztelni kellene) nem ritkán 2000 sor fölé nyúltak, ellehetetlenítve a hatékony unit tesztelést.

Szinte semmilyen technikai szabályozás nem volt érvényesítve, sem a TypeScript fordító, sem a linter részéről, ami borzasztó hanyag kódot és kódolást eredményezett.

A dolgot súlyosbította, hogy a közös kódbázison dolgozó frontend fejlesztők 4 különböző csapatba voltak szétosztva, nem voltak egységes irányelvek lefektetve, sem kikényszerítve, nem volt semmilyen technikai vezetés (ami korábban volt, az bár ne is lett volna). A közhely a közös lóról tökéletesen megállta a helyét, ráadásul állatorvosi is volt.

A code reviewnak semmilyen kultúrája nem volt. Amikor odakerültem, visszanéztem az útóbbi 3 hónap pull request-jeit és nem találtam összesen 20 kommentet. Nincs az a tökéletes projekt, amelyik 3 hónap alatt (mondjuk naponta 4 PR-ral ez kb. 240 PR ennyi idő alatt) csupán ennyi kommentet indukál, lehetetlen. Aztán az első általam review-zott PR-ra egyből raktam 15-öt, mert annyi sebből vérzett.

A projekt az összeszedetlensége okán teljesen indokolatlan 3rd party libekkel volt tele. Az Angular alapból biztosít számformázási lehetőségeket, de már a böngészőben is van erre natív támogatás. Ennek ellenére egy 7 éve karbantartatlan libet sikerült erre felhasználniuk. Azaz, már a projekt indulásának pillanatában 2 éve magára volt hagyva ez a lib, bármely józan fejlesztő azonnal nemet mondott volna az installálására. Nem is beszélve a biztonsági problémákról, amik ezekben a csomagokban rejtőzhetnek - a biztonsági audit stabilan 20 fölötti számot mutatott bármely pillanatban.

A hanyagság további problémákat szült: statikus fájlok voltak commitolva a repositoryba (képek, pdf-ek, videók, stb), amik miatt a 30 MB-os projekt git repója 2GB méretű volt.

A state management, ahogy fent említettem, a komponensekben történt, leginkább class property-k felhasználásával. Ez a megközelítés évek óta ellenjavallt a framework szerint, mert kézzel fogható teljesítményproblémákhoz vezet. Ha valahol mégis értelmes módon volt ez kezelve, ott annyi féle minta volt, ahány előfordulás - zéró konzisztencia ebben is.

A projekt belső függőségeinek térképe (import-gráf) egyetlen kusza pókháló volt, minden mindennel összedrótozva, ráadásul tele god-objectekkel.

A stílusozás (CSS) szintén átláthatatlan módon volt megoldva, esetleges volt reszponzív kinézet is, ami aztán rendszeresen visszaütött, amikor valamit módosítani kellett.

Egy csomó külső függőségnek nem voltak meg a típusfájlai vagy wrapper libjei - gyakorlatilag futásidőben töltődtek be és az alkalmazás teljesen rá volt utalva a nem várt változásokra és tippelgetésekre. Volt, amikor az éppen aktuális, minifikált javascript kódot kellett debugolni egy funkció miatt, mert nem volt dokumentálva sem a 3rd party által.

A hab a tortán a branching stratégia esetlegessége, követhető élesítési folyamat hiánya, dedikált webszerver helyett egy content management rendszer használata meglehetősen unortodox módon.

A dilemma

A projekt a jelenlegi állapotában egy szakmai zsákutca, egy fekete lyuk, amibe mérnökök értékes óráit és motívációját égetjük mindenféle fejlődés nélkül. Borítékolható a fluktuáció/elhanyagolás spirál további mélyítése. "Ne panaszkodj annyit, oldd meg!" - neki is láttam.

A megoldás

Elkötelezett voltam abban, hogy ezen változtatni kell. Összegezve a problémákat elkezdtem gondolkodni egy megoldási lépéssorozaton, amit, ha kell, akár egyedül is végig tudok vinni, de nyilván minél többen állunk neki, annál hamarabb tudunk végezni.

A kód formájának konzisztenssé tétele. Ez egy hosszú és unalmas folyamat, aminek része egy előre felállított szabályrendszer alkalmazása minden egyes fájlra, ami a projektben van. A szabályrendszer nagyon sok pillérből áll, kezdve a hivatalos Angular kódolási javaslatokkal, clean kód szabályokkal, linter és fordítási szabályok bevezetésével és egyedi, a projektre szabott szempontokkal.

Típusok bevezetése mindenhova. A dolog elég trükkös, mert megköveteli a kód részleges megértését és némi kísérletezést, utánajárást, tesztelést.

A kód könnyed átstruktúrálása (fölösleges kódok kidobása, egybe tartozó dolgok egymás mellé rendezése a későbbi mozgatást előkészítendő).

Kitakarítani a fölöslegesen behúzott libeket, helyettesíteni azokat, amiknek semmi keresnivalója a projektben vagy elavultak/megbízhatatlanok (lodash, jquery, stb.).

Teljesen megérteni a nagyobb komponensek szerepét, szétdarabolni őket a felelősségi körök alapján. A feldarabolás során átalakítani a belső működésmódjukat úgy, hogy a legkevesebb state managementre legyen szükség. Azaz a class propertyk állítgatása helyett pure function-ök alkalmazása (csak input és output, semmi side-effect). Ezzel lehetővé válik a unit tesztelés, a performanciaoptimalizálás és további komponens-átszervezés.

Megszűntetni a belső state-eket a komponensekben, bekapcsolni az OnPush CDS-t. Szintén megszűntetni a stage management-et a service-ekben, azok helyett globális state managert használni (NGRX), ahol erre mégis szükség van.

Átstruktúrálni a kódot úgy, hogy világosan meg lehessen különböztetni a prezentációs komponenseket az üzletiektől - ezzel lehetővé válik a kiszervezés és akár egy-egy ilyen modul egy komplett csapathoz rendelése: sokkal világosabb felelősségi körök és számonkérhetőség, gyorsabb hibajavítás, stb.

Az eredmény

Mivel nem voltam kinevezett lead ezen a projekten, a hatásgyakorlásnak egyéb módjait kellett alkalmaznom. Volt például céges Wiki, amibe felvezettem a komplett tervet, kidolgozva az első néhány nagyobb mérföldkövet. Ezt aztán felhasználtam, amikor a code review során olyan módosításokat láttam, amik nem követték ezeket a sorvezetőket. Természetesen a dokumentációk mindenki számára hozzáférhetőek, szerkeszthetőek és vitathatóak voltak, erre mindenkinek előre fel is hívtam a figyelmét.

Elkezdtem RFC-ket gyártani (Request for Comments) azokról a dolgokról, amik újdonságot jelenthetnek a projekt életében vagy egy régi gyakorlatot challenge-elnek meg. Például keressünk glyph-font alternatívát (mivel a meglévő ingyenes szolgáltatás a beetetés után fizetőssé vált), vagy döntsük el, hogy egy új funkcióra milyen külső libet akarunk használni (milyen feltételek mentén vizsgáljuk meg, hogyan döntjük el, stb.), és további ~10 másik. Az RFC-k lényege az volt, hogy minél többen kritizáljuk meg az ott írtakat egy jussunk egy olyan konszenzusra, amit a legtöbbünk helyesnek gondol.

Az egészet prezentáltam több körben az üzleti döntéshozóknak, időbecsléssel, hatásokkal, várható eredményekkel. A tartalmat úgy formáltam, hogy érhető legyen annak is, aki nem annyira technikai, mint amit a projekt jellege megkíván. Bár minden felmerülő kérdést sikerült teljesen tisztázni, a cégnek nem volt profilja a szoftverfejlesztés, így egy szponzor sem mert mögé állni (annak ellenére, hogy a terv arról szólt, hogy a napi munkával párhuzamosan folynának a refaktorálási tevékenységek).

Nem vagyok az a fajta, aki könnyen beletörődik a ignoranciába, így szponzor nélkül is, értelemszerűen nagyon lassú tempóban, de elkezdtem a napi munka részeként átrágni magamat a problémás részeken. Amikor a cost-cut elérte a szerződésünket, úgy 15%-át sikerült a kódnak úgy rendbetenni, hogy arra a későbbiekben is építeni lehessen.

Rendbe tettem és optimalizáltam a CI/CD pipeline-t is, hogy ne 50 percig fusson egy deploy, csak 3-ig. Ez a fejlesztőknek is sokkal gyorsabb visszajelzést adott a módosításaikkal kapcsolatban (a projekt jellege miatt nem lehetett mindent a saját gépen ellenőrizni). Dokumentáltam az egész fejlesztési és élesítési folyamatot is, ami addig szájról-szájra módszerrel, minden alkalommal torzulva jutott egyik fejlesztőtől a másikig.

Gyakran csináltam live-coding eseményeket, amikor megmutattam a kollégáknak, hogy hogyan is gondolom mindazt, amit leírtam, illetve demonstráltam is. Tartottam külön session-öket, ha bármi egyéb témában merült fel kérdés (pl. hatékony unit-tesztelés; az SVG lehetőségei és korlátai; projekt specifikus 3rd party toolok használata; stb.).

Mit nyertem ezzel?

Szigorúan tech skillek szempontból az ég világon semmit, 100% kidobott idő volt. A projekt végig megőrizte a legacy jellegét, egy igazi soul grinder volt az utolsó percig (nem csak technikai-, de management szempontból is).

Amit a projekt hasznára írok az az, hogy elkezdtem jobban dokumentálni, hatékonyabban és érthetőbben beszélgetni nem-technikai emberekkel. Részt vettem folyamatoptimalizációs tevékenységekben, közösséget építettem, előadtam. Sokkal nagyobb méretekben gondoltam át a szoftver dimenzióit, mint az eddigi projektjeim során. Megterveztem egy al-projektet, aminek a célja az volt, hogy egy elhanyagolt, legacy frontendet modernizáljunk és készítsünk fel arra, hogy hosszú távon, a fluktuációt letörve, karriert építve tudjon rajta dolgozni az összes fejlesztő.