Áttekintés
A szoftverfejlesztésnek van egy olyan területe, amely többnyire nem kap elég hangsúlyt: ez a szoftverminőség. Az idő előrehaladtával szerencsére egyértelmű javulás tapasztalható ezen a területen, de még messze vagyunk az ideális állapottól. A szoftverfejlesztés más területeit a rövid távú érdek vagy a kényszer hangsúlyossá teszik (gondoljunk pl. az intergrált fejlesztőeszközök nyújtotta kényelemre vagy a szoftverbiztonsági problémákból adódó kényszerre), a minőség viszont egy nehezen megfogható dolog, melynek a haszna is közvetett. Egészen pontosan: a jó minőségű szoftvernek önmagában nincs is haszna, hanem a rossz minőségű szoftver okozhat óriási károkat. És itt nem véletlenül használtam a feltételes módot: okozhat, de nem biztos, hogy okoz. Emiatt a menedzserek nem szívesen áldoznak erre erőforrást, pénzt, mivel mindig van valami fontosabb.
Az akadémiai világnak viszont a szoftverminőség egy mondhatni kedvenc témája: hatalmas mennyiségű publikáció született ezen a területen. (Az én PhD-m is a téma egy kis szegletéből készült.) A Nemzetközi Szabványügyi Hivatal, az ISO is foglalkozott a témával, megalkotta a 9126-os számú szabványt, melyet azóta felváltott a tovább fejlesztett 25010-es. Az eredeti szabvány a szoftverminőségnek az alábbi 6 területét definiálta:
- Funkcionalitás (functionality): ez azt jelenti, hogy a szoftver valóban azt csinálja, amit magáról állít. Részei: megfelelés, pontosság, együttműködés (más rendszerekkel), biztonság. A megfelelés valószínűleg a minőség legfontosabb része, mert ha mondjuk egy rajzoló programmal nem lehet rajzolni, akkor nincs értelme a programnak.
- Megbízhatóság (reliability): adott körülmények között adott ideig adott teljesítményt nyújt. Részei: érettség, visszaállíthatóság, hiba tolerancia. Ha egy szoftver kezdetben jól működik, de nem sokkal a telepítés után használhatatlanná válik, az adatokat nem lehet kimenteni és visszaállítani, akkor az nem megbízható.
- Használhatóság (usability): arról szól, hogy a szoftver célközönségének mennyi energiára van szüksége ahhoz, hogy használni tudja a szoftvert. Részei: érthetőség, tanulhatóság, kezelhetőség, vonzó külső. Gyakran előfordul, hogy egy, funkcionálisan jól és megbízhatóan működő szoftver annyira bonyolult, hogy nem szívesen használjuk.
- Hatékonyság (efficiency): a szoftver által használt erőforrásokról szól. Részei: idő és erőforrás. Elvárjuk egy szoftvertől, hogy hatékonyan bánjon az erőforrásokkal, mert az nem elfogadható, hogy ha a legegyszerűbbnél egy picivel bonyolult műveletet hajtunk végre, akkor minden erőforrást "felzabál".
- Karbantarthatóság (maintainability): arról szól, hogy mennyi munkára van szükség ahhoz, hogy módosítsunk rajta. Részei: elemezhetőség, módosíthatóság, stabilitás, tesztelhetőség. Egy szoftver a legritkább esetben tekinthető véglegesnek; az idők folyamán új igények merülnek fel, vagy hibák jönnek elő. Ha túl komplikált a kód, nehéz áttekinteni, adott helyen történő módosítás nem várt hatást vált ki máshol, gyakran elszáll, nehézkes kipróbálni, akkor drága és veszélyes lehet a legegyszerűbb továbbfejlesztés is.
- Hordozhatóság (portability): a szoftver különböző környezetekbe történő telepíthetőségéről szó. Részei: alkalmazhatóság, telepíthetőség, együttlétezés, cserélhetőség. Az nem jó szoftver, ha csak adott operációs rendszer adott verziójának adott nyelvi változatával működik, nehézkes a telepítése, bizonyos szoftvereket nem szabad feltelepíteni, hogy használhassuk, és lehetetlen kiváltani mással.
Ezek egyben a szoftver nemfunkcionális részei. Érdemes felsorolni azokat az elemeket, amelyeket az Oracle definiál. Ez részben átfedésben van a fenti szabvánnyal; vannak elemek, amelyeket a szabvány egy alkategóriaként kezel, és olyan is van, ami nem része a szabványnak.
- Teljesítmény (performance): ez alatt válaszidőt kell érteni, tehát pl. mennyi ideig kell várni egy képernyőkattintás után a válaszra.
- Skálázhatóság (scalability): azt jelenti, hogy a terhelés növekedésével is ki tudja szolgálni a rendszer a kéréseket. Mivel egyre nagyobb mennyiségben keletkezik az adat, ennek a jelentősége csak fokozódik.
- Megbízhatóság
- Rendelkezésre állás (availability): ideális esetben azt jelenti, hogy folyamatosan elérhető. A valóságban ez egy arányt jelent, hogy az idő hány százalékéban érhető el a rendszer ténylegesen. Pl. az nem elfogadható, hogy egy rendszer leálljon és elérhetetlen legyen napokon át, de mondjuk egy alapnak számító, 99,5%-os rendelkezésre állás már igen. Egyébként A tipikus rendelkezésre állásnak a formája 99, 9..95%. Ezt egy számmal adják meg, ami a kilencesek száma az 5-ös előtt.
- Kiterjeszthetőség (extensibility): anélkül lehessen új funkciót hozzáadni, hogy a már meglevő funkcionalitást módosítani kelljen.
- Karbantarthatóság
- Kezelhetőség (manageablility): ez alatt az adminisztrátori szintű kezelést kell érteni, hogy folyamatosan biztosítani lehessen a többi nemfunkcionális követelményt.
- Biztonság (security): ne lehessen a rendszert veszélyeztetni.
Clean Code
Robert C. Martin Clean Code című könyve az áttekinthető forráskód alapműve, emiatt adtam ezt a címet ennek a szakasznak. Nem célom a könyv ismertetése, hanem annak koncepciója: az, hogy nagyon fontos, hogy a kód olvasható legyen.
Íme a saját listám!
- Kódolási konvenciók: nincsenek kőbe vésett szabályok, kivéve egyet: legyen konzisztens! Érdemes a profiktól is lesni: a Google kódolási stílusa pl. az alábbi: http://google.github.io/styleguide/javaguide.html.
- Behúzás: döntsük el, hogy tabulátorokat vagy szóközöket használunk, ill. ez utóbbi esetben mekkora legyen. Vannak programozási nyelvek (pl. Python), mely megköveteli a pontos formát, de többnyire szabad kezet kapunk. Kezdetben a tabulátor híve voltam, ma inkább 4 szóközt használok.
- Blokk eleje és vége: a kérdés az, hogy hova tegyük a blokk elejét jelző elemet (általában kapcsos zárójelet): az előző sor végére vagy egy új sor elejére. Én az előz sor végére szoktam tenni, mert úgy szoktam meg, az a gyakoribb, és tömörebb kódot ad, de a másiknak is van létjogosultsága, pl. az, hogy ugyanolyan mélyen van a nyitó és a csukó zárójel. Válasszunk egyet, és legyünk konzekvensek!
- Egy osztály elrendezése: itt arról van szó, hogy milyen sorrendben legyenek az attribútumok, konstruktorok, lekérő és beállító függvények, egyéb függvények, generált kód stb. Én a felsorolt sorrendben szoktam megadni. Ill. ide sorolnám még azt is, hogy a tagolás aszerint történjen, hogy előbb jönnek a publikus, utána a protected, végül a privát metódusok, vagy a csoportosításuk logikai (én ez utóbbit szoktam alkalmazni). A legfontosabb itt is a konzekvencia.
- Központozás: arról szól, hogy hova teszünk szóközt. Ezeket a szabályokat egy picit erősebbnek érzem a többinél, melyek a következők. A kulcsszó és az azt követő zárójel közé mindig szóköz kerül. Ha a vessző ill. pontosvessző után van még a sorban valami, akkor mindig van szóköz közöttük, előtte viszont soha. Az egyenlőségjel előtt és után mindig van szóköz. A műveletek körül van szóköz. A függvénynév és a paraméterlista között nincs szóköz.
- Kisbetű és nagybetű: a függvénynév kisbetűvel kezdődik, és ún. Camel Case módon folytatódik, tehát az új szó kezdőbetűje benne nagybetű, így: ezEgyFüggvény(). A változó név szintúgy (beleértve az osztály attribútumokat és a függvények paramétereit is). Az osztálynév nagybetűvel kezdődik, utána Camel Case. A csomag név csupa kisbetű. A felsorolás típus, valamint a szöveg konstans csupa nagybetű. Az már vita tárgya lehet, hogy ha egy rövidítés fordul elő mondjuk egy függvénynévben, akkor azt hogyan használjuk, pl. formatToJson() vagy formatToJSON(); én az elsőt használom (ennek az oka az, hogy ha a kis kezdőbetűs változó elejére kerül egy ilyen, akkor a jsonFormattedValue szerintem jobban néz ki, mint az, hogy jSONFormattedValue), de itt is a konzisztens használat a lényeg.
- Maximális sorszélesség: személy szerint nem szeretem lefixálni a sor szélességének a maximumát. Kezdetben pl. 80, később 120 karakter volt, de pl. a mostani monitoromon kb. 200 karakter kényelmesen elfér. Ezt a fejlesztőre érdemes bízni. Vannak pl. olyan sorok, melyeknek az olvashatósága lényegtelen: pl. egy debug log üzenet megformázása: az lehet akármilyen széles, nem érdekes. De lehetne olyan részek, ahol fontos, hogy jól lássuk az egészet, ott kell a sortörés úgy, hogy egy átlagos monitoron ne kelljen mozgatni a szöveget.
- Felsorolások: ez szorosan kapcsolódik a maximális sorszélességhez, és az a dilemma, hogy mondjuk egy függvény paraméter listát egy sorba tegyünk-e, vagy a paramétereket egymás alá. Itt nem használok túl éles szabályt: ha úgy látom, hogy a felsorolás jól áttekinthető egy sorban is (pl. 2-3 elemből áll), akkor ezzel tömörítem a kódot. De ha áttekinthetetlenné válik (pl. 10 elemű lista), akkor egymás alá írom, mégpedig úgy, hogy mindegyik elem pontosan ugyanazon a behúzáson legyen, és előtte csak szóközök legyenek. (Egyes kódgenerátorok pl. az első paramétert rögtön a nyitó zárójel után teszik; ha a többi paraméter is új sorban van, akkor az első is kerüljön új sorba.)
- Elnevezések
- Minden név legyen informatív! Kerüljük az olyan változóneveket, hogy a, b vagy l1 (egybetűs változóneveket kizárólag ciklusváltozóként használjunk: i, j és max. {k}}). Az olyan egybetűs változóneveket különösen kerüljük, amelyek összetéveszthetőek valamilyen számmal, pl O, l vagy I. Nyugodtan használhatunk hosszú, több szóból álló megnevezéseket, a kezdeti programozási nyelvek korlátai már rég nem érvényesek. Személy szerint én különösen hosszú neveket szoktam adni a változóknak, függvényeknek, hogy messziről "sikítson", miről van szó.
- Szófajok: az osztálynév legyen főnév, a függvénynév ige, a változónév főnév, a csomagnév adott része főnév (ez ne legyen összetett szó).
- Ne tegyünk bele felesleges információt a változónévbe, pl. ami a típusára vonatkozik, vagy felesleges prefixeket.
- Legyen a név kiejthető! Amikor valaki olvassa a kódot, akkor magában úgymond "felolvassa"; ügyeljünk arra, hogy ezt meg tudja tenni.
- Az egyes nevek jelentősen különbözzenek egymástól! Ügyeljünk arra, hogy ne hozzunk létre hosszú neveket úgy, hogy csak 1-2 karakterben térnek el egymástól. Ránézésre legyen egyértelmű, hogy mire vonatkozik.
- Gondoljunk arra, hogy ha rákeresnénk később valamire, akkor mire keresnénk rá. A megnevezések legyenek kereshetőek. Így a gépelési hibákra is ügyeljünk!
- Függvények
- Egy függvény legyen rövid! Ez számomra olyan, hogy jó, ha rövid, de nem mindenáron. Mert ha van egy mondjuk 200 soros függvény, ami egy jól meghatározott feladatot csinál, azt nyilván szét lehetne szabdalni sok pár soros függvénybe, de azokat szét kellene szabdalni több osztályba, mert az se jó, ha túl sok függvény van egy osztályban, ahhoz már külön csomagot kell létrehozni, és 2-3 ilyen függvény átszervezésének egy közép program érzetű lesz az eredménye. Tehát számomra ideális esetben egy függvény nem több 10 sornál, de számomra ez nem szigorú szabály.
- Egy függvény egyetlen dolgot csináljon! Persze az, hogy mi számít egyelten dolognak, már a programozóra van bízva. Számomra belefér az, hogy felépíti az adatkapcsolatot, elküldi az adatot és lebontja az adatkapcsolatot, feltéve, hogy mindez csak itt történik.
- Egy függvénynek ne legyenek mellékhatásai! Persze elkerülhetetlen, hogy egyetlen függvénynek se legyen mellékhatása, mert pl. egy adatbázisba írás is mellékhatás, de azok legyen elkülönítve. A legtöbb függvény úgy épüljön fel, hogy lehessen egységtesztelni.
- Egy függvénynek ne legyen túl sok paramétere. Általában a 3 vagy több paramétert nem szokták szeretni; számomra ez olyan, hogy lehetőleg ne legyen sok paramétere. De kivételes esetek lehetségesek.
- Az elnevezések legyenek informatívak! Itt elsősorban a függvénynévre és a paraméterek neveire gondolok, de a típus is fontos lehet. Arra is gondolni kell, hogy a hívó oldalon jól olvasható legyen a kód. Gondoljunk csak bele, hogy egy kód átnézése során ezt találjuk: x = f(1, true, null); fogalmunk sincs, hogy ez micsoda! Pl. a paraméter valamilyen szám vagy logikai helyett lehet felsorolás típusú.
- A függvénynek ne kelljen a hibás (pl. null) értékekkel foglalkoznia! Ha egy paraméter egy értéket nem vehet fel, és mégis azt kapja, akkor az a hívó és nem a hívott hibája legyen, a hívó oldalon javítsuk!
- A visszatérési érték mindig a fő sikeres forgatókönyv eredménye legyen, és sohasem valamilyen hibakód. Az egy rossz gyakorlat, hogy a visszatérési érték a lefutás sikerességét jelzi, és a tényleges eredmény mellékhatásként jelenik meg egy globális változóban. Ha hiba lép fel, akkor arra használjuk a kivételkezelést!
- Kivételkezelés: mindig olyan kivételt kapjunk el, amit egy meghívott függvény dob, és nem mi magunk, valamint csak olyat, amivel tudunk is mit kezdeni. Amivel nem tudunk érdemben mit kezdeni, azt dobjuk automatikusan tovább. Létezhet egy olyan céges szabály, hogy a külső vagy rendszer kivételeket be kell csomagolni egy saját, projekt specifikus kivételbe. Ezzel én nem értek egyet, de ha a projekt úgy kívánja, betartom.
- Ne legyenek a kódban elérhetetlen függvények, ill. függvényen belül elérhetetlen programsorok, azokat töröljük ki.
- Osztályok
- Egy osztály egyetlen témára fókuszáljon! Ha oda nem illő függvények vannak (pl. string feldolgozás az adatbázis kapcsolatért felelős osztályon belül), akkor nehéz megtalálni.
- Legyen erős a kohézió! Ez gyakran (de nem féltétlenül csak) abban nyilvánul meg, hogy a függvények egymást hívogatják, sok függvény hivatkozik ugyanarra az osztály változóra. Ha egy osztályban több, egymástól független rész van, akkor megfontolandó az, hogy külön válasszuk őket.
- Legyen gyenge a kötés az osztályok között! Ha túls ok kapcsolat van két osztály között, ráadásul kétirányú a kapcsolat, akkor megfontolandó a két osztály valamilyen értelembe vett összevonása.
- Megjegyzések: ezek a kódnak azon részei, amelyek csak az embernek szólnak, a gép figyelmen kívül hagyja. (Majdnem mindig.) A programozási nyelvek dönt többségében lehet megjegyzést írni. A klasszikus iskola azt mondta, hogy nem tudunk túl sok megjegyzést írni, írjunk amennyit csak akarunk, még az is kevés lesz. A Clean Code mást állít, és ezt maximálisan osztom.
- A megjegyzés rossz! Próbáljuk meg úgy megírni a kódot, hogy egyáltalán ne kelljen megjegyzést fűznünk hozzá.
- Persze vannak kivételek. Az egyik eset, amikor mindenképpen megengedett a megjegyzés, az a publikus elemek Javadoc jellegű kommentelése: ebből generálódik ugyanis a dokumentáció. Viszont azokat a komponenseket felesleges Javadoc megjegyzésekkel ellátni, amelyekről nem szeretnénk dokumentációt generálni. A másik, ami szerintem megengedett az az, amikor egy tényleg nem nyilvánvaló kódrészletet szeretnénk magyarázni. Pl. egy első ránézésre logikátlan, ugyanakkor fontos megoldás magyarázata. Vagy ha egy függvény, osztály nem nyilvánvaló, hogy pontosan mit csinál.
- Ne soroljuk fel a verziótörténetet! Régebben gyakorlat volt az, hogy a programforrás elején felsoroltuk a változásokat; hogy ki mikor mit adott hozzá. Erre ott van a verziókövető, a forráskódba ne tegyünk ilyesmit.
- Sok esetben egy hosszú copyright szöveggel indul a kód, amit én teljesen feleslegesnek gondolok, úgyse olvassa el senki.
- Ne maradjon a forrásban kikommentezett kód! Bátran töröljük ki, az megmarad a verziókövetőben.
- Elrendezés: arról szól, hogy milyen legyen a kódkép.
- Alapvetően szeretem a tömör kódot, melyben minél kevesebb a felesleges dolog. Az elnevezések jó hosszúak, így többnyire megjegyzés sem kell.
- Vízszintes elrendezés: az, hogy milyen széles a kódsor, számomra attól függ, miről szól az a sor. A maximális sorszélesség kérdésről már volt szó, magamtól az ott leírtakat alkalmazom.
- Függőleges elrendezés: a pár soros függvényt üres sor nélkül készítem, de ha hosszabb a függvény, akkor üres sorokat hagyok a nagyobb logikai egységek között. Két függvény között mindig hagyok egy üres sort. Ha létezik a függvényeknek logikai csoportosításuk, akkor a csoportok között kettőt is. Általában nem szeretem a rövid megjegyzéseket a függvények előtt, mert többnyire nem túl informatívak, viszont rontják az összképet. Az osztály attribútumait általában üres sorok nélkül szoktam felsorolni de ha több logikai egységbe lehet őket sorolni, akkor hagyok egy üres sort a logikai egységek között.
- Egység teszt: miket vegyünk figyelembe a unit tesztek írásakor?
- Lefedettség: a kód minél nagyobb területét fedjük le, akor triviális egységtesztekkel.
- Egy unit teszt felépítése: egy logikai tesztelést hajt végre. Ez persze nem minden esetben jelenti azt, hogy pontosan egy assert hívás van. Jól elkülönül a felépítés, a teszt végrehajtása és az eredmény kiértékelése.
- F.I.R.S.T: Fast (gyors), Independent (független), Repeatable (ismételhető), Self-validating (önértékelő; ez azt jelenti, hogy az eredménye egyértelműen igaz vagy hamis); Timely (az elkészítés időpontjára vonatkozik: az üzleti logika megvalósítása előtt hozzuk létre az egységtesztet).
Az is egy jó kérdés, és ide tartozik, hogy az integrált fejlesztőeszközök általában nyújtanak automatikus kódformázást, ami beállítható. (Meglepő egyébként, hogy az alapértelmezett formázási stílus nemigazán szép; a legegyszerűbb az lenne, ha mondjuk az Eclipse és az IntelliJ IDEA közösen tökéletesre csiszolná az alapértelmezettet, és mindenki azt használná.) Ennek a kötelezővé tétele viszont nem jó szerintem, melynek több oka is van:
- Lelassítja a fejlesztő környezet beállítást. Ill. mivel nincs semmilyen mechanizmus, ami ezt kikényszerítené, óhatatlanul előfordul, hogy valaki elfelejti.
- Mindig vannak "renitens" fejlesztők, akik nem a megszokott eszközöket használják. Hiába van egy tökéletesre csiszolt Eclipse szabályhalmaz, ha valaki NetBeans-t használ.
- Nagyobb rendszereknél a generált kód kikerülhetetlen. Ez esetben viszont jó kérdés, hogy a generáltat használjuk-e, vagy a fejlesztőkörnyezet által átformázottat. És az szinte törvényszerű, hogy valakinél el fog csúszni.
Valójában érdemes megegyezni a szabályokban, de a túl szigorú átverés a rendszeren kontraproduktív.
A szoftverminőséget elősegítő eszközök
A SonarQube a legnépszerűbb olyan eszköz, amely statikus kódelemzést végrehajtva hívja fel a figyelmet a kódolási szabálysértésekre. A használata kissé nehézkes. A telepítéshez hajtsuk végre az alábbi lépéseket:
- Töltsük le a programot innen: https://www.sonarqube.org/downloads/, majd csomagoljuk ki.
- Indítsuk el a bin\windows-x86-64\StartSonar.bat fájlt.
- Nyissuk meg a http://localhost:9000 oldalt és lépjünk be admin felhasználónévvel és admin jelszóval.
- Hozzunk létre egy új projektet (ezt a későbbiekben a jobb felső sarokban található + ikonra kattintva tehetjük meg)
- A létrehozáskor felajánlja, hogy generáljunk egy tokent, ezt tegyük meg. Ezt meg kell adni a Maven-nek paraméterül, ahogyan azt a rendszer kiírja, pl. nálam:
mvn sonar:sonar -Dsonar.projectKey=adder -Dsonar.host.url=http://localhost:9000 -Dsonar.login=fdd56032c9d74a8c2a626c78df39be853521bccb
- Az eredmény automatikusan megjelenik a fenti oldalon: mindenféle statisztikák, valamint a kódolási szabálysértések.
Alapértelmezésben - ahogy erre figyelmeztet is - egy beépített adatbázisba menti az adatokat. Produkciós környezetben ezt célszerű lecserélni egy másikra. Számos programozási nyelvet támogat, a Java támogatása a legszéleskörűbb. Alapértelmezésben egész sok szabály található benne, bár a súlyosság megállapítása sokszor igen furcsa. Kétségkívül óriási előnye viszont a kiterjeszthetőség: számos kiegészítő létezik hozzá, pl. ez: https://www.sourcemeter.com/.
Code review
Az egyik legjobb módszer a kódminőség megfelelő szinten tartására a kód review. Ez nemcsak amiatt hasznos, mert "több szem többet lát", hanem mert ha a fejlesztő azzal a tudattal fejleszt, hogy a végén nemcsak funkcionálisan kell megfelelően működnie, hanem a kód minőségét is meg kell védenie, akkor ösztönösen igyekszik jobb kódot készíteni.
A kód review-t is számos eszköz segíti, nekem a Crucible-lel, valamint az azt támogató FishEye-jal van némi tapasztalatom. De külön eszköz nélkül is tudunk kód review-t tartani.