Az infokukac blog 2010. márciusában átköltözött. Átirányítás folyamatban...

Az átirányítás automatikus. Ha mégsem sikerülne, látogasd meg az új oldalt a http://infokukac.com címen, és frissítsd a könyvjelzőidet!

2009. július 30., csütörtök

A dokumentáció

A szoftverfejlesztési projektek terméke nem csupán maga a szoftver, hanem annak dokumentációja is. Dokumentációt általában azért kényszerülünk írni, hogy az ügyfél ez irányú kérését kielégítsük, vagy pedig azért, hogy a cégünk belső szabályozásainak eleget tegyünk.

A dokumentáció célja a szoftverrel kapcsolatos ismeretek átadása: követelmények és az elvárt működés kommunikálása (pl. követelményspecifikáció, funkcionális specifikáció), a különféle tervezési eredmények rögzítése (pl. logikai, fizikai rendszerterv), az elkészült rendszer használatának bemutatása (felhasználói kézikönyv), üzemeltetéssel kapcsolatos feladatok ismertetése (üzemeltetői kézikönyv), a projekt haladását ismertető dokumentumok (státuszriportok) stb.

Mikor jó, ha dokumentációt írunk?

Még mielőtt rátérek a dokumentációgyártás-ellenes nézeteimre, le kell írnom, hogy a megfelelő dokumentációkra igenis szükség van. Kiváló példa erre a felhasználói kézikönyv. A felhasználói kézikönyvre egyszerűen szükség van. (Ámbár érdemesebb feltenni magunknak itt is a kérdést: biztos, hogy a felhasználói kézikönyv a megoldás? Nem jobb-e egy megfelelő oktatás, e-learning tananyag, inkább csináljunk-e on-line súgót esetleg GYIK-ot? Egy felhasználói kézikönyvet nem olvas el az elejétől a végéig úgysem senki.)

Az üzemeltetői kézikönyv is egy fontos dokumentum: az üzemeltetői állomány számos emberből áll, akiknek több rendszert is kell egyidejűleg felügyelniük és üzemeltetniük. Ők nem ismerik (és nem is kell ismerniük) a rendszer belső működését, de igenis tudniuk kell, hogy milyen teendőik vannak, hogy biztosítsák egy rendszer folyamatos működését. Ők tipikusan egy rövid, tömör dokumentumot várnak.

Ha egy dokumentumot akarunk készíteni, mindig őszintén tegyük fel magunknak a következő kérdéseket:

  • Mi a célja a dokumentumnak? 
  • Ki fogja (el)olvasni?
  • Lesz-e haszna?
  • Mi történik, ha mégsem írom meg?
  • Tényleg ez az az időpont, amikor meg kell írnom?

Egy dokumentáció jó megírása megfelelő képességet és gyakorlatot igényel. (Erről talán majd máskor.) 

Miért nem jó a dokumentáció?

A szoftver tervezési, implementálási döntéseit leíró dokumentáció elkészítése a következők miatt problémás:

  • nem olvasható ki belőlük a szándék: a követelményspecifikáció nem írja le az ügyfél szándékát, csak "bután" rögzíti az ügyfél oldaláról elhangzott összes követelményt. A tervezéssel kapcsolatos dokumentációk szolgai módon igyekeznek kielégíteni az ismeretlen kontextusban felmerült követelményeket, és a tervezési döntések mögött meghúzódó gondolatmenetet, - ami a fejlesztő számára szükséges lehet - nem kommunikálja. A fejlesztő így egy olyan anyagból kénytelen dolgozni, amelynek hátterével nincs tisztában. (Ez azért fontos, mert ha enyhítenénk a követelmények és tervezési döntések kényszerein, sokkal egyszerűbb és jobb megoldáshoz juthatunk. A jobb megoldást úgy is lehet érteni, hogy ha egy funkciót egyszerűen, gyorsan valósítunk meg, más fontos funkciók kifejlesztésére is több idő jut.) 
  • a követelmények (elvárások) _mindig_ változnak: ha következetesek és "up-to-date-ek" akarunk maradni, akkor állandóan frissíthetjük a dokumentációt;
  • nem lehet előre jól (és nem is érdemes) megtervezni a szoftvert, túl komplex az elvárt működés;
  • túl sok idő megy el a tervek elkészítésével ahelyett, hogy egy működő szoftvert készítenénk, amit az ügyfélnek bemutatva azonnali visszajelzést kaphatnánk;
  • az ügyfél nem fogja megérteni a dokumentációt;
  • a fejlesztő nem érti meg a dokumentációt: ez lehet a dokumentációt készítő hibája, a fejlesztő hibája (hiányossága), de általában egyik hibája sem. Ha az írott szöveg túlságosan informális, akkor bizonyos részletek sínylődnek el, ha pedig túlságosan formális, akkor már a lényeg veszlik el, ráadásul nem ad mozgásteret a fejlesztőnek. Ki fog megérteni, és fejben tartani több száz oldalnyi írott szöveget?;
  • ha e-mailben, dokumentációkban kommunikálunk egymással, folyamatos a félreértés lehetősége. Különösképp akkor bosszantó ez, ha csak későn derül ki, hogy valamit félreértettünk.

Akik preferálják a dokumentumok gyártását, a következőkkel érvelnek:

  • Ha nincs leírva, mit kell csinálni, 
    • hogyan állapodunk meg és számolunk el az ügyféllel?
    • hogyan lesz megtervezve a szoftver?
    • hogyan lesz lefejlesztve a program?
    • hogyan lesz letesztelve az elkészült szoftver?
    • hogyan tudjuk követni a projektet? Honnan tudjuk mikor leszünk készen?
  • Egyesek szerint olcsóbb, ha elkészül a részletes tervezési dokumentáció, mintha a fejlesztés, tesztelés közben/után derülne ki, hogy valami nem megfelelően készült el.
  • Ha nagy csapatban dolgozunk, elengedhetetlen, hogy minden le legyen dokumentálva.
  • Ha a fejlesztőt elüti a villamos, hogy fogják tudni folytatni a munkáját?

Dokumentációs elvárások minimalizálása

Egyszerűen vannak olyan helyzetek, amikor a dokumentáció elkészítése nem kerülhető ki. Ha az ügyfél mindenképpen részletes dokumentációt akar, próbáljuk meggyőzni, hogy azt utólag, a szoftver elkészítése után készítjük el, mert ő jobban jár, ha a többhónapos tervezés helyett már egyből egy működő szoftvert lát. Az ügyfél üzleti megrendelőit általában nem érdekli a dokumentáció, a szoftvert szeretnék látni, és minél hamarabb bevezetni, ezért náluk kulcsoljunk. (Az ügyfél IT, PM, QA, SEC-es szakembereit nehéz erről meggyőzni, de jó esetben az üzlet drive-olja a projektet, nem pedig a backend-szervezetek).

Ha a cégünkön belül vannak dokumentációs elvárások, akkor indítsunk egy mozgalmat, amivel elérhetjük céljainkat. Győzzük meg munkatársainkat, főnökeinket, hogy jobban járunk, ha a dokumentációs rendet enyhítjük. Persze, ez óriásvállalatoknál, ahol központilag szabályozzák a módszertanokat ez szélmalomharc lehet.

A dokumentációkkal szembeni elvárásokat csökkenthetjük, de ez önmagában nem elegendő. Nagyon fontos, hogy a hagyományos szoftverfejlesztési módszertantól elrugaszkodjunk, és egy jobb módszert találjunk a projekt közben kommunikáció elősegítésére, jobbá tételére.

Cross-functional, kis létszámú csapat

Ahelyett, hogy fázisokra bontanánk a projektet, és az egyes fázisokhoz más-más embereket rendelnénk (követelményfelmérés, -elemzés, tervezés, fejlesztés, tesztelés), akik a fázisok között adják át a következő szereplőknek a tudást, hozzunk létre egy olyan kis létszámú csapatot (max. 7-9 fő), amely a teljes projektet képes véghezvinni (ezért cross-functional). A csapat nagy része fejlesztő legyen, akiknek a program megírásán túli feladata az üzleti probléma megértése. Rengeteg dokumentálás spórolható meg, a kommunikáció sokkal hatékonyabbá válik, mert a fejlesztői csapat képviselői közvetlenül beszélnek az ügyféllel, és nincs szükség közbülső szereplőkre, akik fordítanak az üzleti oldal és a fejlesztői oldal között. (És ezt ráadásul dokumentációhegyekben kommunikálják.)

A csapatot célszerű egy projekt szobába ültetni, ahol a tagok bármikor, könnyen információt cserélhetnek.

On-site customer

Az ügyfelet és annak üzleti problémáját ismerő személy (business expert) ideális esetben a csapat tagja. Ha kérdés merül fel, a csapat bármelyik tagja közvetlenül hozzá fordulhat. Nem kell levelezni, meeting-elni, workshop-olni, emlékeztetőket írni az ügyféllel, az elvárások könnyebben formálhatóak át egy működő szoftverbe.

A kód maga a dokumentáció

Mi írja le a legrészletesebben a szoftver működését? A szoftver kódja maga. Törekedjünk arra, hogy a forráskód jól szervezett, jól olvasható legyen! Ha ezt elérjük, akkor nem, vagy csak minimális dokumentáció szükséges a program kódjához. A következők segíthetnek abban, hogy a kód önmagát dokumentálja:

  • kódolási konvenciók,
  • code review-k,
  • _jó_ kommentezés,
  • folyamatos refaktorálás,
  • Test Driven Development (TDD).

A TDD alkalmazása kulcsfontosságú

  • Az automatizált tesztek forgatókönyvként is felhasználhatóak. Egy-egy üzleti forgatókönyv leírása mentén világosan látszik, hogy a szoftver mely részei érintettek a funkciók megvalósításában, és azok hogyan működnek együtt egymással, hogyan valósítják meg együtt a kívánt működést. Ez egy kiváló módszer, hogy végig kövessük, mi hogyan működik a programban, és ezáltal a (teszt)kódban dokumentáljuk a program működését. Új fejlesztői tagok bevonása esetén a tesztek tanulmányozása, új tesztesetek megírásának kikényszerítése felgyorsítja az új csapattag tanulási, beilleszkedési folyamatát.
  • A kód folyamatos refaktorálásával, átalakításával áttekinthetőbbé, érthetőbbé tehető a kód. Ha a refaktorálás közben hiba csúszik be, akkor a tesztek (optimális esetben) jelzik azt.
  • Megfelelően megírt automatizált tesztesetek esetén a manuális tesztelés feladatát lényegesen leegyszerűsíthetjük, így a tesztelői csapat számára nem kell mindig a teljes rendszer működését újra és újra ledokumentálni.

Egyszerű, átlátható módszerek a projektkövetésre

Tegyünk le arról, hogy a projekt elején megbecsüljük, mikorra leszünk kész, és hogy pontosan tudjuk, mit fogunk megcsinálni! (Tudom, ez így messzire vezet. Olvassátok Csuti blogját!) Helyette inkább alkalmazzunk olyan egyszerű módszereket, amelyek elősegítik a projekt átláthatóságát, tervezhetőségét. A SCRUM eszközei lehetőséget nyújtanak rá:

  • Sprint és demó: az 1-4 hetes iteráció után az ügyfélnek bemutatásra kerülnek a sprint során elkészült funkciók. Ennek közvetlen előnyei: 
    • az ügyfél látja, mi készült el, és közvetlenül reagál rá (a státuszriportot lényegében elfelejthetjük),
    • a tesztelői csapat is követni tudja, mi készült el, a tudást már menet közben felszívják,
    • a fejlesztői csapat tagjai is követni tudják a rendszer fejlődését.
  • Reggeli stand-up: mindenki beszámol, mit tett előző nap, mit tervez mára, milyen problémákkal szembesült.
  • A product backlog: az összes előre látható feladat egy egyszerű táblázatban, prioritás szerint van felsorolva. Folyamatosan követhető, mi készült el, és a prioritások megfelelő változtatásával biztosíthatjuk, hogy időre elkészüljön egy működő release.

Ezen módszerek biztosítják a projekt folyamatos személyes követhetőségét, és a projektterv akár teljes mellőzését.

Belső wiki

Vannak olyan információk, amelyek nem forráskódban lelhetőek fel, de elérhetővé kell őket tenni a teljes fejlesztői csapat számára. Ezeket célszerű wikiben tárolni. Többek között mi erre használjuk a wiki-t:

  • az egyes sprintekhez készült demó forgatókönyvek tárolása (ezek mentén mutatjuk meg az ügyfélnek az újonnan elkészült funkciókat),
  • fejlesztéssel kapcsolatos útmutatók, pl. kódolási konvenciók, Maven, SVN tudnivalók, IDE beállítások,
  • a fejlesztéskor használt tesztkörnyezetek elérhetősége,
  • ...

Nem egyszerű

Sok helyen hallani sem akarnak arról, hogy ne a hagyományos módon vezetett projektmódszertanok szerint haladjunk. Az elvárás jogos: ha pontosan leírjuk a követelményeket, akkor mindkét oldal el tud számolni egymással. Az ügyfél megmondja mennyit hajlandó fizetni, a szállító pedig elfogadja (vagy nem). Később lehet tudni, hogy a megrendelő mit követelt, és a szállító mit teljesített. Ha eltérés van, akkor azt kezelik.

A felelősség ezen a szinten jelenik meg először, és ez tovább gyűrűzik. Valaki leírta a követelményeket, ő dolga végeztével átadja az elkészült anyagot a tervezőknek. Az ő munkájuknak, és felelősségüknek ott a vége, hogy lerakják a tervezéssel kapcsolatos dokumentációkat. A fejlesztők között kiosztják a munkát, mindenki a neki szóló dokumentumrészt implementálja, azért felel. A tesztelők számára is elkészül a dokumentáció, ami alapján a tesztelést végzik, ők ezért felelnek.

A szoftverfejlesztés extrém módon komplex, és magában hordozza az extrém bizonytalanságot is. Lehetetlen előre rögzíteni a követelményeket, lehetetlen előre tervezni, becsülni. Helyette olyan módszereket kell alkalmaznunk, amelyek képesek alkalmazkodni a változásokhoz, és kezelhető velük a bizonytalanság.

Az írott dokumentáció helyett elő kell segíteni a verbális kommunikációt. Ez alapvetően megváltoztatja a hagyományos módszertanok által sugárzott gondolkozást. Ha verbálisan kommunikálunk, akkor ezt csak kislétszámú csapatban tehetjük meg. Ha nincs dokumentáció, amit tételesen ellenőrizhetünk, hogy végeztünk-e a munkával, más módszereket kell alkalmaznunk.

Számos kérdés merül fel, és jelenleg nincs általános megoldás. Mégis van egy kulcs: önmagunk folyamatos megfigyelésével, értékelésével és tanulással mindig adaptálódjunk az adott környezethez!

2009. július 18., szombat

Domain Driven Design 1. rész - mi ez?

Már több, mint két éve, hogy megismerkedtem a Domain Driven Design-nal (DDD), azóta is projekten alkalmazzuk munkatársaimmal a filozófiát. Úgy döntöttem, hogy egy cikksorozatot indítok ebben a témában. A cikksorozat célja a filozófia népszerűsítése.

Mi is ez?

A filozófia lényege az üzleti probléma orientált megközelítés. A filozófia alkalmazása szoftverfejlesztési projekt esetében a következőket fogja eredményezni:

  • megismerjük az adott üzleti területet, amelyhez a szoftvert készítjük,
    • a teljes fejlesztői, tesztelői csapat megismeri az üzleti területet,
  • elsajátítunk és kialakítunk egy mindent átható nyelvet (Ubiquitous Language), amely a teljes projekt kommunikációját megalapozza, ezáltal:
    • az ügyféllel egy közös nyelvet fogunk beszélni,
    • a teljes fejlesztői, tesztelői csapat is ezt a nyelvet fogja használni,
    • a nyelvet nem csak szóban, hanem írásban is alkalmazzuk a forráskódban és a dokumentációkban,
  • és végül: minőségi szoftver készül el.

Ha tényleg minőségi szoftvert szeretnénk készíteni, értenünk kell ahhoz az üzleti területhez, amit a szoftverrel támogatni kívánunk. Véget ért az az idő, amikor a fejlesztők magányosan meghúzódva a specifikációk alapján a boxaikban kódoltak. A teljes fejlesztői, tesztelői csapatnak meg kell értenie, mi az, amit csinál, és hogy hogyan gondolkozik az ügyfél.

Mitől új?

Egyrészt nem új, mert eddig is voltak olyanok, akik évtizedek ezelőtt úgy fejlesztettek szoftvert, hogy ezeket az elveket ismerték. Azért sem új, mert különféle megközelítésekből is erre a következtetésre lehet jutni. Gondoljunk a Scrum-ra, ahol minden sprint közben azon dolgozunk, hogy az ügyfél elégedettségét a demón kivívjuk, vagy gondoljunk az XP-ben az on-site customer szerepkörre. Arról nem is beszélve, hogy az agilis megközelítéseknek mind az ügyfélcentrikusság az alapja.

Másrészt azért új a filozófia, mert Eric Evans megírta a bibliát. (A könyv elég hosszú, 500+ oldal, készült belőle egy ingyenesen letölthető kivonat.) A 2004-ben megjelent könyv tudatosan rendszerezi mindazokat az ismereteket, amiket Eric a szoftverfejlesztési projektekben ezzel a hozzáállással megtapasztalt. A rendszer magja a Domain Model, amelynek feladata, hogy a komplex üzleti tudást és a leendő szoftver működését, ismereteit hatékonyan (értsd: jól érthetően) rendszerezze. Domain Model-t sokféleképpen lehet kommunikálni, de elsődleges megjelenési formája maga a forráskód.

A Domain Model

A könyv utal arra, hogy hibás a hagyományos vízesés modellben lefolytatott projektek megvalósítása:

  • Követelményeket gyűjtögetünk, ezt leírjuk egy többszáz - ne adj, Isten - több ezer oldalas doksiban. (Ki fogja ezt elolvasni és megérteni? Mi garantálja, hogy jó lesz ez az anyag?);
  • A "nagy" szoftvertervezők összepattintják a szoftver modelljét, terveit (újabb pár száz oldalas doksi, telepakolva mindenféle UML-diagramokkal; vagy esetleg valamilyen MDA tool-lal készített szörnyeteg, amiből már le is lehet generálni a forráskódot, és ebbe a "niggerek"-nek már csak be kell írnia a kódot);
  • Az elkészített tervek alapján a fejlesztők elvégzik a munkát;
  • Ha a fejlesztők végeztek, a tesztelők tesztelnek, megtörténnek a hibajavítások;
  • ...és KÉSZ.

Ez a fajta hozzáállás egyirányú kommunikációt eredményez. Valaki leírja a követelményspecifikációt, ezt a terméket átadja a tervezői csapatnak, a tervezői csapat pedig a terveket adja át a fejlesztői csapatnak (pl. logikai rendszerterv), akik végül a szoftvert a tesztelői csapatnak nyújtják át. Elég sokan rájöttek már arra (de sokan még mindig sötétségben élnek), hogy ez szinte soha nem működik, de legalábbis nem eredményez minőségi szoftvert. (Viszont kiválóan védheti vele mindenki a saját s***ét.)

A fejlesztőknek és az üzleti megrendelőnek folyamatosan együtt kell működnie. Nincs szükség áttétekre, közbülső szereplőkre. Nem kell olyan, aki több száz oldalas doksikat gyárt. Ezek helyett az emberek helyett inkább bízzuk a tehetséges fejlesztői csapatra a megvalósítást.

Sokkal előnyösebb az a megközelítés, hogy a rendszer modelljét már egyből a forráskódban megvalósítjuk, és van üzleti szakértőnk, aki segít abban, hogy a modellel kapcsolatos - folyamatosan felmerülő - kérdésekre választ kapjunk. Az üzleti szakértő valóban ért a problémához, rendszerint ő maga az ügyfél.

A DDD-vel végrehajtott projektek során a Domain Model folyamatosan fejlődik (ahogy a fejlesztők dolgoznak rajta), átalakul, áttöréseken esik át, és újabb szinteket ér el az üzleti megismerésnek és a megvalósításnak a folyamatában. A Domain Model egy az egyben tükrözi a fejlesztői csapat tudását az adott üzleti területről.

A Domain Model folyamatos evolúciójának létjogosultsága a következő problémák miatt indokolt:

  • a követelmények állandóan változnak: az ügyfél nem tudja előre megmondani, hogy pontosan mit szeretne,
  • (általában) nincs az az agy, aki egy rendszert előre meg tudna tervezni, mert:
    • túl komplex,
    • az ördög mindig a részletekben rejlik: problémák hada implementáció közben jön ki (technológiai és üzleti problémák), ezek visszahatnak a már meglévő elképzelésekre, tervekre, a kódbázisra;
    • a felmerülő üzleti problémákra tudnunk kell a válaszokat ahhoz, hogy tovább léphessünk. Ezeket a problémákat a projekt elején nem is lehet látni, és csak a szoftverben folyamatosan fejlődő modellben jelentkeznek.

Mikor alkalmazzuk a DDD-t? Hogyan csináljuk?

(folyt.köv.)


[Update: A cikk második része megtekinhető itt.]

2009. július 13., hétfő

Az alul- és túltervezett kód

A mai posztban két jelenségről az under engineering-ről és az over engineering-ről írok. Ha le kell fordítanom őket, akkor az alultervezettség ill. túltervezettség fogalmakat használom. (Ha valaki jobb fordítást tud, szívesen veszem.)


Under engineering

Egy alultervezett kód fő ismérve, hogy igencsak nehezen érhető meg (gány), és ezáltal igen nehézkes a továbbfejlesztése, karbantartása. A gyakorlatban ez igencsak sűrűn előfordul. A fejlesztő nem látja a "big picture"-t, a lokális problémákra fókuszál (rendszerint azokat is rosszul oldja meg), és ezáltal nem képes a kódját rendszerezetten felépíteni.

Teljesen természetes dolog, amikor egy probléma megoldásának elején nem látjuk még, hová vezet az út, és a program belső struktúráit nem megfelelően építjük fel, de ez nem mentség arra, hogy a kód így is maradjon.

Fontosabb az, hogy ahogy haladunk a programmal, éberen őrködjünk a saját és fejlesztőtársaink programkódja felett, és ismerjük fel (vagy el!), ha valami "bűzlik". Ilyenkor ne sajnáljuk az időt a refaktorálásra, és rakjuk rendbe az érintett kódot. Ha rendben van a kód, könnyebb azt tovább fejleszteni, hibát javítani benne, és ezáltal időt, pénzt és idegességet(!) takarítunk meg.

Ez persze, nem megy könnyen, mert ki akarna belepiszkálni olyan kódba, ami már "működik". Ennek a helyzetnek a megelőzésére (is) ajánlott alkalmazni a Test Driven Development-et, és a kódunkhoz automatizált teszteseteket írni. (Ennek mikéntjéről majd máskor.) Ha vannak tesztjeink, amik bármikor futtathatóak, akkor magabiztossággal fejlesztünk tovább vagy alakítunk át egy meglévő kódbázist, és nem az jut eszünkbe, hogy "ezt még bottal se piszkálnám".

Kereshetjük a kifogásokat (Joshua Kerievsky, Refactoring to Patterns):

  • nincs időnk arra, nem szánunk elég időt rá, vagy nem adnak elég időt arra, hogy refaktoráljunk,
  • gyorsan kell új funkciókat fejleszteni a rendszerbe,
  • ...

...és a végén marad az alultervezett kód.

Over engineering

Egy túltervezett kód a következők miatt alakul ki:

  • ismerek egy tervezési mintát, ami a megoldandó feladatra _a_ "szép megoldás" (legalábbis azt képzelem),
  • általánosan akarom megoldani a feladatot, mert mégiscsak programozó matematikus (vagy ... - mindenki egészítse ki magának) vagyok,
  • nem ismerem a pontos üzleti elvárásokat, ezért megcsinálom a programot úgy, hogy majd a későbbiekben is jó legyen.

A túltervezett kód legalább olyan rossz, mint az alultervezett. Bonyolult a program, rengeteg az áttét, és akár olyan szintű absztrakció is jelen lehet a kódban, amely a valósághoz már nem köthető. Az eredmény nem csupán nehezen értelmezhető kód, és a jól "kitalált", "általános" megoldás még be is szűkíthet minket. Állandóan azt kerülgetjük, szenvedünk, ahelyett, hogy új, üzletileg értékes funkciókkal bővítenénk a szoftverünk tudását. 

Míg az alultervezett kód kis részekben, folyamatosan refaktorálható, a túltervezett kód rendszerint magasabb szintekre gyűrűzik fel, és átalakítása az alkalmazás egy nagyobb részét érinti. Ezért a már megírt, túltervezett kódra épülő kódbázist nem könnyű felszámolni...

...és a végén marad a túltervezett kód.

A megoldás: KISS és YAGNI

A KISS princípium (Keep It Short and Simple) lényege, hogy mindig annyit csináljunk meg, amennyire szükség van (röviden és egyszerűen), és közben folyamatosan refaktoráljunk. A készülő kód funkcionalitásban konvergál az elvárt felé, és mivel azt folyamatosan karbantartjuk (és nem bonyolotítjuk túl), a lehető legegyszerűbb megoldáshoz juthatunk. 

Az ortodox XP-ből (eXtreme Programming) származó YAGNI-elv lényege a túltervezett kód ellen ad útmutatásokat: "Úgysem lesz rá szükséged" - mondja ("You Ain't Gonna Need it."). A kísértés, hogy olyan kódot írjunk, amely nem szükségszerű, a következő hátrányokkal jár (Wikipédia):

  • A hasznos funkciók kifejlesztésétől, tesztelésétől és továbbfejlesztésétől vesszük el az időt.
  • Az új funkciókat nyomkövetni, dokumentálni és tesztelni kell.
  • Minden új funkció bizonyos hatással lesz a jövőre, ezáltal egy most kifejlesztett, szükségtelen feature megakadályozhatja egy későbbi szükséges funkció megalkotását.
  • Ameddig nincs szükség egy adott funkcióra, nehéz megmondani, hogy pontosan hogyan is kellene annak működnie. Ha pedig ez fennáll, nem várható el, hogy az helyesen is működjön, még akkor sem, ha egyszer szükség lesz rá.
  • A szoftver egyre nagyobb és bonyolultabb lesz.

Ugyan az elvek pofonegyszerűnek tűnnek, de észre sem vesszük, hogy valójában rengetegszer döntünk velük ellentétesen. Elsajátításuk nem egyszerű, alkalmazásuk önmérsékletre, következetességre int. Figyeljük magunkat és munkánkat! A code review-k is segíthetnek abban, hogy egy bűzlő kódrészletet utólag külső szereplőként is áttekinthessünk, és fejlesztőtársaink visszajelzései is mutatják, ha valami nincs rendjén.



2009. július 10., péntek

Mire jók az assertek?

Tesztesetek írásakor a rendszerben lévő metódusokat hívjuk, és ellenőrizzük, hogy a kívánt eredményt kapjuk-e. Tipikusan assert-et és ennek variációit használjuk, hogy teszteljük, hogy az általunk elvárt eredmény megegyezik-e a tényleges eredménnyel. Nagyon egyszerű, de nem túl realisztikus példa:

    public class Multiplier {
        private int x;
        private int y;
    
        public Multiplier(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int doCalculation() {
            return x * y;
        }
    }

A teszt kód pedig:

    public class TestMultiplier {
        @Test
        public void test3By5() {
            Multiplier m = new Multiplier(3, 5);
            assert m.doCalculation() == 15;
        }
    }

Hogyan működik egy assert?

Ha az assert után megfogalmazott logikai kifejezés hamis eredményt ad, a környezet exception-t dob. Ez tipikusan a tesztkód futásának sikertelenségét eredményezi, amelyet a fejlesztői környezet rendszerint egy piros bucival jutalmaz. (Ha lefut a teszt, akkor a teszthez tartozó buci zöld lesz.)

Alaphelyzetben Java forráskód fordításánál az assert-eket be kell kapcsolni a -ea kapcsolóval. (Microsoft platformon is alaphelyzetben csak debug fordítói üzemmódban "élnek".) Ha nincsenek bekapcsolva az assert-ek, akkor platformtól/nyelvtől függően vagy 

  • eleve nem kerülnek bele a lefordított kódba ezek az utasítások (pl. C, C++), vagy
  • a futtató környezet (pl. JVM) átlépi őket.

Assert-ek a produkciós kódban

Az assert-eket nem csupán a tesztelést végző kódban használhatjuk, hanem "elszórhatjuk" őket a produkciós kódban is. Ennek célja a futás közbeni elő- és utófeltételek ellenőrzése, és az invariánsok kifejezésre juttatása, és ezek ezáltal a program biztonságosabbá tétele. (Az invariánsok olyan kifejezések, amelyeknek az értéke a program végrehajtása közben ugyanaz.)

Példák:
  • (előfeltétel) van egy Book osztályom, amely rendelkezik egy sell metódussal (ez akkor hívódik, ha a könyvet eladják). Egy könyvet nem lehet többször eladni, és ezért normál esetben a sell metódus sem hívható meg egynél többször (mert a GUI biztosítja, hogy ne lehessen ráklikkelni egy már eladott könyvre). Ha biztosak akarunk lenni benne, hogy egy eladott könyvre nem hívják meg többet a sell metódust, kezdjük így a metódust: assert !sold; (A sold boolean mező jelzi, hogy a könyvet eladták-e.)
  • (invariáns) van egy egészekből álló listám, amelyben az elemek összege mindig 100. A listába magam teszek be elemeket, és veszek is ki belőle, de szeretnék biztos lenni abban, hogy soha nem tévedek, és az összeg valóban 100. Ezt új elem hozzáadásakor vagy meglévő törlésekor egy megfelelő assert-tel biztosíthatom.

Sokan idegenkednek az assert-ek használatától: nem értenek egyet azzal, hogy a tesztkódon kívül is használatban legyenek, ill. ha használatban is vannak, a produkciós környezetben mindenképpen legyenek kikapcsolva. Ezt a következő érvéléssel támasztják alá:

  1. Nem valók a tesztkódon kívüli kódba, és beszennyezik azt.
  2. Az én kódom hibamentes, és ezt a unit (vagy más) tesztekkel igazolom.
  3. Az assert-ek csökkentik a produkciós kód futási sebességét.


Az első érvvel igazából nem tudok mit kezdeni. Sajnos, a Java, C# és nagyon sok más nyelv sem ad lehetőséget arra, hogy peremfeltételeket fogalmazzunk meg az egyes metódusok, függvények alkalmazhatóságát illetően.

A második nem igaz: hiába is lenne a unit tesztek általi kódlefedettség 100%-os, ez egyáltalán nem garancia a hibamentességre (erről külön post-ban igyekszem majd írni). A harmadik állítás pedig természetesen igaz, azonban az esetek többségében a lassulás nem észrevehető.

Az én érvem az assert-ek tesztkódon kívüli használatára az, hogy ezáltal előírhatjuk azokat az elő- ill. utófeltételeket, ill. invariánsokat, amelyeket az egyes metódusok futásakor elvárunk, és ennek hatására csökkentjük az adatkorrupció lehetőségét, ugyanis ha a produkciós környezetben a feltétel nem teljesül (tehát olyan állapotba kerül a program, ami a program írásakor szerintünk nem volt elképzelhető), a program exception-t generál, és alapesetben a tranzakciók visszagörgetődnek. A rendszer visszaáll egy (remélhetőleg) stabil állapotba.

Ha nincsenek ilyen biztonsági mechanizmusok beépítve a programunkba, akkor könnyen szembesülhetünk azzal, hogy az adatbázisban tárolt adatok egy része értelmezhetetlen lesz, és a program futása is "eltéved" (a felhasználók megmagyarázhatatlannak tűnő hibabejelentéseket produkálnak). Sokszor fordul elő, hogy a hiba csak később derül ki. Ráadásul a program alkalmazása során a hiba hatása tovább gyűrűzik, és végül még több adatot szennyez be. Ilyenkor adattisztító programokat/szkripteket írhatunk, hogy helyre tegyük az adatbázist (ha tudjuk egyáltalán, hogy mi az, ami elromlott), és persze az eredeti hibát is ki kell javítanunk, hogy ne fordulhasson elő még egyszer. A programhiba megtalálása az eltorzult, korrupt adatok alapján még nehezebb.

Miért ne legyen biztonságosabb a kódunk, és ha valami nem várt körülményt észlelünk, akkor azonnal leállunk?

Szándékaink kifejezése

Az assert-ek tesztkódon kívüli használata nem csupán belső állapotellenőrzésre alkalmazható, hanem többi fejlesztőtársunk felé is kommunikálhatjuk vele feltételezéseinket. Nekem nagyon sokat segített már az, hogy a régebben saját vagy más által megírt kódban az assert-ek képesek voltak az akkori fejlesztői elme gondolatát még hatékonyabban közvetíteni. 

Az assert-ek tudatos használa során nem csupán egy implementáció fogad bennünket egy metódus elolvasásakor, de a megfogalmazott állítások egyfajta kontextusba is helyezik a metódust.

Mikor ne használjunk assert-eket?

Az assert-ek tesztkódon kívüli használata csak belső állapotellenőrzésre szolgálhat. Nem szabad alkalmazni akkor, amikor a "külvilág" bemeneteire reagálunk pl:

  • input (pl. UI-ról vagy Web Service-en keresztül érkező) adatok validálásakor: ilyenkor - az alkalmazott technológiától függően - specializált exception-ökkel vagy visszatérési értékkel kell tudatnunk a klienssel a hibát.
  • API írásakor: ez ugyanaz mint az előző eset. Gondoljunk példaként a Java SDK-ra, ahol egy üres ArrayList-ből akarjuk törölni a 10. elemet. Az API exception-nel válaszol.



2009. július 8., szerda

Tömbök, mint value-objectek

A programozási nyelvek legtöbbje lehetőséget biztosít függvények (metódusok, procedurák) alkalmazására. A függvények hívásakor paramétereket adunk át. Ezen átadások tipikusan kétféle: referencia szerinti (vagy cím szerinti, by reference), ill. érték szerinti átadás (by value). (Vannak más fajták is.)

Az érték szerinti átadás

Ez az egyszerűbben megérhető. Vegyünk egy Java vagy C# metódust, amely paraméterként egy int-et vár:

    void f(int x) {
        // ...
        x = 10;
        // ...
    }

// ...később egy másik metódus belsejében
    int z = 8;
    f(z);
    f(5);

Az f(z) hívás közben a z értéke lemásolódik, és amíg az f függvény végrehajtása tart, az x változóval erre a másolatra (lokálisan) hivatkozunk. Ha az x értékét megváltoztatjuk a függvény belsejében (ami amúgy refaktorálási szempontból nem egy jó szokás), akkor a másolatot módosítjuk, ennek közvetlenül nincs hatása az eredeti z változóban lévő értékre.

Referencia vagy cím szerinti átadás

A következő kód C++-ban írodott:

    void f(int &x) {
        // ...
        x = 10;
        // ...
    }

// ...később egy másik metódus belsejében
    int z = 8;
    f(z);
    f(5);                // fordítási hiba

        

Itt az f(z) híváskor a z változót tároló memóriarekesz (kezdő)címe adódik át. Ennek az a következménye, hogy a függvényen belüli x-re irányuló változtatás közvetlenül megváltoztatja az eredeti változó értékét is. Az f(5) hívás egyenesen fordítási hibát eredményez, mert szemantikailag helytelen. (Hogyan változtatható meg az 5 konstans értéke? Kell változó.)

Na, ez most melyik?

A következő kód Java-ban íródott:

    void buy(Customer customer) {
        customer.addOrder(this);
        // ...
        customer = null;
        // ...
    }

// ...később ugyanebben az osztályban egy másik metódus belsejében
    Customer c = ...;
    buy(c);

Tudjuk, hogy a buy metódus végrehajtása közben ugyanazzal az objektummal dolgozunk, mint ami a híváskor jelen van, de a metódusban a customer változó null-ra állítása nem befolyásolja a c értékét. Úgy tűnik, mintha kettőséggel szembesülnénk: bár ugyanazon az objektumon dolgozunk, és ez referencia szerinti átadást sejtet, viszont ha magát a customer változó értékét változtatom meg, az eredeti c változó nem változik. Ez utóbbi viszont érték szerinti átadást mutat.

Valójában Java-ban minden érték szerint adódik át. A c ill. customer változók referenciák (mintha C-beli mutatók lennének), ezen referenciák értéke másolódik le függvényhíváskor. Így a függvényhívás belsejében a referencia változik meg (már máshová mutat).

Value object-ek

Láthatjuk, hogy Java-ban objektumra történő hivatkozást (referenciát adunk át). A primitív típusokon (pl. int, long) kívül mindent referencián keresztül érünk el, azonban bizonyos esetekben célszerű a referenciákon keresztül elért objektumokra ún. "value object"-ként gondolni. Value object-re tipikus példa a dátum, ill. sztring, vagy a komplex számok és a pénzmennyiséget reprezentáló struktúrák. Ezekre általában úgy tekintünk, mint a primitív típusokra, műveleteket is hasonlóképpen végzünk velük. (A C#-ban a string primitív tipus, Java-ban az osztályt ellátták a final módosítószóval, amelynek eredményeképpen az eredeti String objektumon nem tudunk változtatást végezni, egy konkatenáció pl. mindig egy új String-et ad vissza.)

A tömbök, mint value object-ek

Képzeljük el, hogy van egy Customer osztályunk. Ebben az osztályban deklaráltunk egy addresses mezőt, amely az ügyfél címeit reprezentálja.

    public class Customer {
        private Set<Address> addresses;

        public Set<Address> getAddresses() {
            return addresses;
        }
    }

Ezzel a kóddal kapcsolatban a legnagyobb gond az, hogy ha valaki elkéri egy ügyfél címeit, akkor a visszakapott Set<Address> referencián keresztül közvetlenül módosíthatja azokat (magát a halmazt). Ezt OO szempontból "nem szokták szeretni", mivel az objektumnak kell megvalósítania az egységbezárást, az objektumon történő megfelelő metódushívásoknak kell elvégezniük a módosítást (ez így olyan mintha a mezőt public-ká tennénk.) Miért akarná valaki módosítani a visszakapott referencián keresztül a halmazt?

  • véletlenül: előfordult már, 
  • szándékosan: az ilyen emberektől védeni akarjuk a kódot.

Én minden ilyen esetben, amikor egy halmaz, lista stb. -szerű objektumot adok vissza, lemásolom. Tehát a getter-jeim így néznek ki:

        public Set<Address> getAddresses() {
            return new HashSet<Address>(addresses);
        }

Ennek következménye, hogy aki megkapja az új halmazt, vígan módosíthat benne, és nem cseszi el - se szándékosan, se véletlenül - az eredeti halmazt. Ennek a módszernek annyi a hátulütője, hogy lassít az alkalmazáson, és bizonyos típusú szoftvereknél ez tényleg problémát jelenthet. (Meggyőződésem viszont, hogy ORM-környezetben vagy más "enterprise-környezetben" ha egy halmaznak eleve sok eleme van, és Java-kódból kell feldolgozást végezni rajta, az eleve lassú lesz annak lemásolása nélkül is.)

A tömb lemásolása ilyen szempontból úgy viselkedik, mintha egy value object-et adnék át. Önmagában egy kompakt egész, amelyen lekérdezési műveleteket lehet elvégezni. 

Még egy kis finomítás

Van-e annak értelme, hogy az addresses mező null-referencia? Ha nincs egy eleme sem a halmaznak, akkor az egy üres halmaz. A Set-nek ugyanúgy léteznie kell, legfejlebb üres lesz.

Valamiért utálok egy mezőnek kezdő értéket adni, inkább a konstruktorokban teszem ezt meg. Ennek ellenére a Set-hez hasonló osztályokat mindig deklarációkor inicializálom:

    public class Customer {
        private Set<Address> addresses = new HashSet<Address>();

        public Set<Address> getAddresses() {
            return new HashSet<Address>(addresses);
        }
    }

A primitív típusok esetén is van default érték, tipikusan 0. Itt halmaz esetén az üres halmaz lesz az (ha kell elemet bele pakolni, akkor a konstruktorban még mindig megtehetem). Még tovább: mi történik, ha egyszerre akarom "felülcsapni" az ügyfél összes címét?

    public class Customer {
        private Set<Address> addresses = new HashSet<Address>();

        public void updateAddresses(Set<Address> newAddresses) {
            addresses = new HashSet<Address>(newAddresses);
        }

        public Set<Address> getAddresses() {
            return new HashSet<Address>(addresses);
        }
    }

Az előző gondolatmenet alapján ennek így kell lennie. Ha addresses = newAddresses-t írnék, akkor ugyanaz a probléma állna fenn, mint a getter-eknél. (Ha valaki kívül módosítja utólag a halmazt, az módosul a Customer-ön belül is.)

Collections.unmodfiableSet - még egy kis adalék

Pár hónapja leltem rá a Java SDK-ban a Collections osztály statikus unmodifiableSet (...List stb.) metódusára. A metódus paramétere egy halmaz, visszatérésként egy másik halmazt kapunk vissza, amely nem módosítható. (Ha módosítanánk rajta, akkor UnsupportedOperationException-t kapunk.) Ez nagyon hasonló a mi "kézi" halmazlemásolásunkhoz, de valójában csak egy view-t kapunk vissza. Ha valaki az eredeti halmazt módosítja, az a view-ban is megváltozik.






Címkék:

2009. július 7., kedd

A szoftverfejlesztés művészete újra

Nemrég indítottam ezt a blogot, és még nagyon le vagyok maradva tartalommal Csutihoz képest :) Kb. egy évvel ezelőtt Kulcsival közösen készítettünk egy háromrészes cikksorozatot, amely a "Szoftverfejlesztés művészete" címet kapta. Hogy a blogom terjedelmét kicsit bővítsem, a leendő Olvasóknak hivatkozom be most a cikksorozatot. Az egyik célom az, hogy az említett témaköröket itt a saját blogomon részletesebben körbejárjam, és újabb gondolatokat is ássak elő. Jöjjön tehát a három rész:

Címkék:

2009. július 1., szerda

Az UnsupportedOperationException ereje - hogyan írok új kódot?


Egy fejlesztés alatt álló rendszerben tipikus fejlesztői attitűd az, hogy a kódot alulról felfelé írjuk. Mennyire jellemző dolog is ez:
  1. Csinálok egy osztályt, mert arra majd szükségem lesz;
  2. Csinálok egy metódust, amit majd valamikor meghívok: nagyjából tudom a paramétereit is, akár miért ne írhatnám meg már most is, ha tudom, hogyan kell működnie?;
  3. Felviszek pár mezőt is az osztályba, merthát az adatokat valahol tárolnom is kell;
  4. Rájövök arra is, hogy az osztály valamilyen asszociáción keresztül más osztályokkal is kapcsolatban áll, ha már erre járok, ezt is megcsinálom.
  5. GOTO 1. (Valójában az 1-4. pontokat szabadon, tetszőleges sorrendben alkalmazhatom.)

Hogy hol ér véget a folyamat, azt nem tudni. Az eredmény garantált: nagyon nehézkesen haladok a kóddal, ide-oda kapkodok, és fogalmam sincs, hányadán állok az egész problémával. Ha már tudatos refaktorálónak is érzem magam, akkor tovább bonyolítom a helyzetemet azzal, hogyféligmeddig itt-ott megírt programrészeket alakítgatok. Jó esetben ez a fajta hozzáállás konvergál egy működő (de nem feltétlenül jó) kódhoz.

Szerintem tudod, miről beszélek :)

Vannak olyan ismerőseim, akik a hétköznapi beszéddel is képesek ezt produkálni. Megvan bennem minden jóindulat irányukban, de egy idő után elfáradok: nem tudom, hol kezdtük, merre tartunk, és már fáj a fejem az egésztől.

Hogyan lehet ezt másképpen csinálni?


Amikor új kódot kezdek írni, arra törekszem, hogy először megírjam a hozzá tartozó tesztet. A teszt során állandóan abba a problémába fogok ütközni, hogy olyan metódusokat akarok meghívni, amik még nem léteznek. Semmi gond: a kedvenc IDE-met használva a nem létező metódus fej részét létrehozatom, és a belsejébe automatikusan (Javaban) egy "throw new UnsupportedOperationException();"-t íratok. (C#-ban "throw new NotImplementedException();"-t írok.)

Visszatérek a tesztemhez, és tovább folytatom, amíg nem végzek vele. Amikor futtatom a tesztet, futás közben látom, hogy mik azok a metódusok, amelyeket elfelejtettem megírni, és ezeket pótolom.

Ez a módszer akkor is alkalmazható, ha épp nem tesztből indulok ki, a lényeg, hogy fókuszált maradjak. Nem szabad engedni a kísértésnek, hogy kizökkentsem magam az aktuális gondolatmenetemből. A kódban elhelyezett "bombáim" garantálják, hogy ne felejtsek el később semmit. A fegyelmezettség megéri: mivel mindig egy dologgal foglalkozom, és egy szemszögből nézem a problémát, mindig jobban végig tudom azt gondolni, és ráadásul rövidebb idő alatt is végzek.

Ha többen dolgozunk egy kódbázison, akkor az exception dobásakor egy szöveget is megadhatunk, ami utal arra, hogy az a saját bombánk. Ezáltal könnyen megkereshetjük a kódban (futtatás nélkül is) a félretett gondolatfoszlányainkat.

A probléma ezen megoldása felülről lefelé megvalósítást tesz lehetővé. Tudom, hogy mit szeretnék elérni, a megvalósítás pedig már "részletkérdés".

Címkék: