5. fejezet - Kódújraszervezés

Tartalom

Tesztvezérelt fejlesztés
Kódújraszervezési technikák
Kódújraszervezési eszköztámogatás

Az implementáció során gyakran olyan helyzetbe kerülünk, hogy érezzük, hogy valami nem feltétlenül stimmel vele. Olyan ez, mint amikor kinyitjuk a hűtőt, és hirtelen érzünk valami furcsa szagot. Ez nem feltétlenül jelenti azt, hogy valami romlott, lehet, hogy csak a sajtnak van olyan furcsa szaga, amilyennek lennie kell. Forráskódjaink esetén ez azt jelenti, hogy a kódunk is lehet „gyanús szagú”, amely mélyebb problémákat jelezhetnek, de fontos megjegyezni, hogy egy-egy ilyen gyanús szag nem feltétlenül jelenti azt, hogy a kódunkkal probléma van. A gyanús szag (bad smell) kifejezést programkódra először a ’90-es évek végén, a Refactoring: Improving the Design of Existing Code című könyv ([FOWLER1999]) szerzői használták.

Nagyon sok minden jelezhet potenciális problémákat kódunkkal kapcsolatban. Például egy hosszabb metódus is már bűzlik, hiszen ez azt jelezheti, hogy nehéz gyorsan áttekinteni és megérteni, mit tesz a metódus, sőt, talán több felelősséget is megvalósít a kelleténél. Ez azonban önmagában még nem jelenti, hogy ezzel a metódussal biztosan probléma van: a hossz oka lehet az is, hogy például a grafikus felhasználó felület egy osztályaként sok felületetelemet kell elhelyezni, amely megnöveli a hosszát. A gyanúsan szagló kód felderítése során azonban sok tényezőt figyelembe kell venni. Ilyen tényező például a forráskód nyelve, a fejlesztés során alkalmazott módszertan, stb. Minden bad smell-hez tartozik egy úgynevezett baseline, azaz küszöbérték. A baseline alatti értékek még nem számítanak bad smell-nek, viszont a felette szereplő értékekkel rendelkező kódelemek már igen. Ezeket az értékeket a használt nyelv, az alkalmazott fejlesztőkörnyezet ismeretében kell megválasztani, mivel különböző helyzetekben eltérhetnek a még elfogadható értékek határai.

Egy gyanús szagra adott konkrét példa az úgynevezett Long Method (hosszú metódus) bad smell. Erről akkor beszélünk, ha egy metódus hossza meghalad egy előre definiált küszöbértéket. Ezen érték meghatározásakor figyelembe kell venni a kérdéses programozási nyelvet, mivel nyelvfüggő lehet az, hogy egy adott problémát hány sorban, hány utasítással lehet megvalósítani. Figyelembe kell venni az alkalmazott kódolási konvenciókat is, illetve a rendszer célját és felhasználási környezetét is. Persze a hosszú metódusok nem feltétlenül okoznak komoly gondokat, de mégis célszerű foglalkozni velük (például több, kisebb metódusra bonthatjuk a hosszú metódust), mivel egy rövidebb metódust könnyebb átlátni, ami a karbantarthatóság szempontjából fontos. Az is előfordulhat persze, hogy a fejlesztő tisztában volt azzal, hogy a metódus hosszú lesz, viszont a feladat megoldása megkövetelte a küszöbérték átlépését.

Egy másik gyanús eset, ha úgy találjuk, hogy adatosztályokat használunk (ez a Data Class bad smell). Ez azt jelzi, hogy az osztály szinte csak adattagokat, és azok lekérdező és beállító metódusait tartalmazza, amely szintén nem feltétlenül komoly probléma, vagy potenciális veszélyforrás, de mégis célszerűbb lenne az ilyen adatokat a felelősségek meghatározásának elve alapján felosztani (vagyis az adatok mellé kell rendelni a hozzájuk kapcsolódó funkcionalitást – a rajtuk végezhető műveleteket – is). Számos ilyen, potenciális problémát jelző gyanús szagot dokumentáltak már, [FOWLER1999] (és magyar nyelvű fordítása, [FOWLER2006]) is több ilyet leír.

Fontos tehát megjegyezni, hogy attól, mert egy kód „bűzlik”, nem feltétlenül van vele baj. Ezek a szagok (smell-ek) elsősorban csak felhívják a fejlesztők figyelmét arra, hogy itt valamilyen potenciális hiba bújhat meg. Ha a fejlesztő megvizsgálja a helyzetet, dönthet úgy, hogy ezzel nem foglalkozik, mert esetleg szándékosan lett ilyen a kód (például a hosszú GUI osztály esetén).

Ha azonban úgy ítéli meg, hogy ez bizony tényleg valamilyen probléma jelenlétére utal, akkor a vonatkozó kódrészeket javítani kell. Az ilyen javításokat nevezzük kódújraszervezésnek, vagy -átszervezésnek (refactoring-nak).

A kódújraszervezés (refactoring) az a művelet, amikor a rendszerünk struktúráját úgy változtatjuk meg, hogy annak funkcionalitása nem módosul. A rendszer strukturális változtatásakor nem szükségszerű óriási, komplex változtatásra gondolni. Egy egyszerű osztályátnevezés is kódújraszervezés, ugyanúgy, ahogy egy metódus átalakítása, vagy kibontása. A refactoring minden egyes lépésekor figyelnünk kell arra, hogy a kód működése konzisztens maradjon az eredeti rendszerével. Amennyiben ezt a műveletet kézzel hajtjuk végre, úgy a kockázat is magas, mivel sokkal könnyebben kerülhetnek bele elírások (egy osztály átnevezésekor figyelni kell arra, hogy minden egyes helyen, ahonnan azt osztályt hivatkozzák, átírjuk a nevet), vagy hiányozhatnak az átszervezés lépései. Minden egyes átszervezés után a rendszert tesztelni kell(ene), ami nagyon idő- és erőforrásigényes. Amennyiben ez a művelet automatikusan történik, úgy a hiba kockázata sokkal kisebb, és a tesztelés is sokkal gyorsabban végrehajtható. Tény, hogy az átszervezés nem változtatja meg a szoftver megfigyelhető viselkedését. A szoftver továbbra is ugyanazt a tevékenységet végzi, mint korábban. A felhasználó, legyen az akár végfelhasználó, akár egy másik programozó, nem veszi észre, hogy megváltozott a rendszer szerkezete.

Ez az észrevétel Kent Beck „két sapka” metaforájához vezet el minket. Szoftverfejlesztés során két különböző tevékenység között osztjuk fel az időnket: a szolgáltatások bővítése és az átszervezés között. Amikor új szolgáltatásokat adunk a programhoz, nem változtatjuk meg a meglévő kódot, csak új részeket adunk hozzá. Előrehaladásunkat tesztek létrehozásával és működőképessé tételével mérhetjük. Amikor átszervezünk, akkor szándékosan nem veszünk fel új tevékenységeket, csak átépítjük a kódot. Nem készítünk új teszteket (kivéve, ha egy korábban kihagyott esetet találunk), és csak akkor változtatunk meg egy tesztet, ha erre mindenképpen szükségünk van az interfész esetleges megváltozásának kezelése érdekében.

A szoftver fejlesztése során valószínűleg gyakran kapjuk magunkat „sapkacserén”. Először megpróbálunk a kódhoz adni egy új szolgáltatást, és rájövünk, hogy ez sokkal egyszerűbb volna, ha más lenne a kód szerkezete. Tehát a másik sapkánkat magunkra öltve, eredeti kód fejlsztése helyett egy ideig átszervezési tevékenységeket végzünk. Amikor a kódnak már jobb a szerkezete, ismét magunkra öltjük fejlesztői sapkánkat, és megírjuk az új szolgáltatást. Amint az működőképes lesz, rájövünk, hogy túl bonyolultan kódoltuk, így ismét sapkacsere jön, és átszervezünk. Mindez talán csak rövid ideig (akár csak tíz percig) tart, de fontos, hogy mindvégig tisztában legyünk vele, hogy éppen melyik sapkánkat viseljük.

Kódújraszervezés nélkül előbb-utóbb elkerülhetetlenül romlik a program szerkezete. Ahogy a programozók megváltoztatják a kódot (rövid távú célok megvalósítása érdekében, vagy a kód szerkezetének teljes átlátása nélkül), a kód elveszíti eredeti szerkezetét, és nehezebb lesz azt megérteni. A kódújraszervezés a kód rendbetétele: azért végezzük, hogy eltávolítsunk olyan darabokat, amelyek nem a megfelelő helyen vannak. A kód szerkezetének összekuszálódása halmozott hatásokkal jár: minél nehezebb átlátni a kód szerkezetét, annál nehezebb módosítani a programot és annál gyorsabban romlik a minősége. A rendszeres kódújraszervezés segít formában tartani a kódot.

Egy rosszul tervezett program esetében általában több kód szükséges ugyanazon funkció megvalósításához, gyakran azért, mert a kód gyakorlatilag szó szerint ugyanazt végzi több különböző helyen. Ezért a felépítés javításának fontos szempontja a többször szereplő (ismétlődő) kódrészek eltávolítása. Ennek fontossága a kód jövőbeni módosításakor jelentkezik. A kód mennyiségének csökkentése nem eredményezi a rendszer gyorsabb futását, mindazonáltal nagy változást jelent a kódban. Minél több a kód, annál nehezebb megfelelően módosítani azt, hiszen több kódot kell megérteni. Gyakori probléma, hogy megváltoztatunk egy kódrészletet az egyik helyen, de nem változtatunk meg egy másik részt, amelyik nagyjából ugyanazt a feladatot végzi, csak kissé más környezetben. A megkettőzött kódok kiküszöbölésével azt érjük el, hogy a kód mindent csak egyszer tartalmaz, és ez a jó felépítés lényege (ez összhangban van a DRY alapelvvel).

Az alábbi videók egy kódújraszervezési lépéssorozat alkalmazását szemléltetik:

Tesztvezérelt fejlesztés

Tesztvezérelt fejlesztés (test-driven development, TDD) alatt egy olyan szoftverfejlesztési megközelítést értünk, amely a tesztelés és a kódfejlesztés folyamatát együttesen, egymástól szét nem választható módon, párhuzamosan végzi. A kód kifejlesztése inkrementális módon történik, és mindig magával vonja az adott inkremens tesztjeinek a fejlesztését is. Mindaddig nem léphetünk a következő inkremens fejlesztésére, amíg a korábbi kódok át nem mennek a teszteken.

A tesztvezérelt fejlesztés eredetileg az extrém programozásnak (extreme programming, XP) nevezett agilis szoftverfejlesztési módszertan részeként jelent meg, de sikerrel alkalmazhatjuk nemcsak agilis, de hagyományos (terv központú) szoftverek kifejlesztése során is.

A tesztvezérelt fejlesztés három törvénye:

  1. Tilos bármilyen éles kódot írni mindaddig, amíg nincs hozzá olyan egységtesztünk, amely elbukik.

  2. Tilos egyszerre annál több egységtesztet írni, mint ami ahhoz szükséges, hogy a kód elbukjon a teszt végrehajtásán. A fordítási hiba is bukásnak számít!

  3. Nem szabad annál több éles kódot fejleszteni, mint amennyire ahhoz van szükség, hogy egy elbukó egységteszt átmenjen.

Ez a három törvény azt jelenti számunkra, hogy először mindig egységtesztet kell készítenünk ahhoz a funkcionalitáshoz, amelyet le szeretnénk programozni. Azonban a 2. szabály miatt nem írhatunk túl sok ilyen tesztet: mihelyst az egységteszt kódja nem fordul le, vagy nem teljesül valamely állítása, az egységteszt fejlesztését be kell bejezni, és az éles kód írásával kell foglalkoznunk. A 3. szabály miatt azonban ilyen kódból csak annyit (azt a minimális mértékűt) szabad fejlesztenünk, amely a tesztet lefordíthatóvá és sikeresen lefuttathatóvá teszi.

A legfontosabb tehát a tesztvezérelt fejlesztésse kapcsolatban, hogy megértsük, hogy csupán néhány nagyon egyszerű lépést ismétlünk újra és újra. Ezek az egyszerű kis lépések azonban remek kódolási tapasztalathoz juttathatnak minket. A tesztvezérelt fejlesztés tehát ciklikus tevékenység, mindig ugyanazokat a tevékenységeket hajtjuk végre alkalmazása során. Szokás ezt red–green–refactor cilkusnak is nevezni, mert az egységtesztek grafikus futtatói között bevált színséma segítségével határozhatjuk meg a teendőket:

  1. Először is írni kell egy olyan tesztet, amely elbukik (red, hiszen a grafikus futtatók ezt a piros színnel jelölik).

  2. Ezt követően alakítani kell a tesztelendő rendszeren mindaddig, amíg az átnem megy majd a teszten (green, hiszen ezt zölddel jelölik).

  3. A harmadik lépsben az ismétlődések megszüntetése, a kód újraszervezése (refactoring).

5.2. ábra - A tesztvezérelt fejlesztés ritmusa [KACZANOWSKI2013]

A tesztvezérelt fejlesztés ritmusa [KACZANOWSKI2013]

Fontos, hogy megértsük a szemléletmódot: ha valamilyen megvalósítandó funkcionalitásról gondolkodunk, akkor azt tesztek formájában írjuk le előbb! Mivel ez a funkcionalitás még nem készült el, ezért a teszt nyilvánvalóan bukásra van ítélve (sőt, elképzelhető, hogy le sem fordul!). Az alapötlet itt az, hogy rögtön kezdjünk el a kliens fejével és szemszögéből gondolkodni a rövidesen megírásra kerülő kódunkkal kapcsolatban, és képesek leszünk a valóban fontos dolgokra összpontosítani.

Mihelyst ezzel megvagyunk, jöhet a green lépés: megírjuk a funkcionalitást úgy, hogy a tesztelendő kód átmenjen a teszteken. Fontos, hogy csak kis lépésekben haladjunk! Ne akarjunk túlságosan előrehaladni. Lehetőség szerint mindig csak azt a minimális mennyiségű kódot írjuk meg, ami ahhoz kell, hogy a teszten átmenjen. Nem baj, ha már biztosan tudjuk, hogy az a későbbiekben nem lesz elég, de vegyük figyelembe, hogy lesz egy csomó más tesztünk is, amelyek mind valamilyen funkcionalitás meglétét vizsgálják. Ha túlságosan előre gondolkozunk, fennáll a veszélye, hogy a későbbi döntési lehetőségeink közül veszünk el, a szabadsági fokunkat szűkítjük egy esetlegesen túl korán meghozott döntéssel.

5.3. ábra - A tesztvezérelt fejlesztés ritmusa részletezettebben [KACZANOWSKI2013]

A tesztvezérelt fejlesztés ritmusa részletezettebben [KACZANOWSKI2013]

A harmadik lépés a kód újraszervezése. Miután a tesztjeink sikeresen lefutnak, keresnünk kell a bad smelleket, azon pontokat a kódunkban, amelyek potenciális veszélyforrást jelenthetnek (például az ismétlődő kódrészek sértik a DRY alapelvet, a bonyolult feltételes logikák esetleg nincsenek összhangban a KISS alapelvvel, stb. A tesztjeink az újraszervezés során is jó szolgálatot tesznek: a feladatunk annyi, hogy az újraszervezés során mindvégig zölden tartsuk a futtatási eredményeket (ez látszik a részletesebb ábrán is), hiszen ez jelzi, hogy a kódunk kívülről (a kliens szemszögéből) ugyanúgy viselkedik, mint mielőtt nekikezdtünk az átszervezésnek. Ha bármikor pirosra fordul a jelző, akkor valamit hibáztunk, szóval vonjuk vissza az előző lépést.

Fontos

Nem csak a tesztelendő kódra gondoljunk! Ugyanilyen fontos a tesztkód tisztány és érthetően tartása is, vagyis az újraszervezési lépéseket a tesztosztályainkon éppúgy végezzük el, mint a tesztelt osztályainkon!

Tipp

A tananyaghoz kapcsolódó videók között a Bowling Kata bemutatásán keresztül nyújtunk betekintést a tesztvezérelt fejlesztés mikéntjébe.