Hogyan (ne) programozzunk?

 

Papír – ami elérhető

 

Pár éve, mikor még a Pascal-lal ismerkedtem, kicsit még másképp álltam a programok írásához..

A dolog kb. így festett: bekapcsolom a gépet, diszkrét pittyegés, kisvártatva pergő hang jelzi, hogy mind a 640 kbyte-om a helyén van, és – szinte hiánytalanul - rendelkezésemre áll (kivéve azt a pár tucat bájtot, amit egy időben a Cruel vírus szép csendben lefoglalt magának:°)).

 

A BIOS jelzi, hogy a gép fel- illetve elszállásra kész, végül egy spártai, de barátságos DOS-prompt köszönt ( később, ahogy fejlődött a világ, egyből Norton parancsnok “szalutált”).

Ezután elő a Turbo Pascal-lal, és a szép kék szerkesztőbe beírom, hogy “begin”. Majd rögtön alá, hogy “end”. Aztán elkezdek gondolkozni…

 

Hajdan így fogtam hozzá programjaim írásához, majd rá kellett jönnöm, hogy van ennél jobb módszer is.

 

A kezdeti pár soros programocskákat hamarosan bonyolultabb feladatok követték, és gondolataimat előbb papírra vetettem, rajzolgattam, tervezgettem – teljesen ösztönösen, hisz akkor még nem is hallottam folyamatábrákról.

 

Szokásommá vált, hogy előbb papíron „programoztam“, és csak aztán kezdődött a kódolás. Házi-feladat jellegű „project“-ek nem követelik meg ezt a körültekintést, de amint a kód meghaladja a néhány tucat sort, jobb, ha elgondolkozunk – nem kellene ezt mégis előbb megtervezni?

 

Tervezzünk

 

Elvileg mindenki, aki nem egy húsz soros programot akar összedobni, a tervezéssel kezdi. Ha terv nélkül ugrunk neki egy bonyolultabb alkalmazás készítésének, komoly fejfájásra számíthatunk, kusza, redundáns kódrészletekre, ami egyrészt feleslegesen bonyolulttá teszi a forráskódot - és egyben lassúvá a programot -, másrészt nehézkessé a karbantartást, az utólagos hibajavítást.

 

Természetesen vannak azért a papírnál korszerűbb megoldások, szoftverfejlesztést, modellezést támogató eszközök. Ezek egy része „fizetős“ (pl. „Rational Rose“, melynek igen borsos ára van, igazából cégek engedhetik meg maguknak), de vannak szabadon használható eszközök is.

 

Sajnos (szerencsére?) volt már dolgom szemetes, átgondolatlanul megírt, rosszul strukturált kóddal, mely ráadásul utólag toldva-foldva lett...

Rémálom volt megtalálni és kijavítani benne bármit, ugyanis, ha több napos kimerítő nyomozás után sikerült is megtalálni a hiba okát, azt kijavítva újabb, addig rejtett hibák jöttek elő.

 

Mivel általában időre el kell készüljön a hibajavítás, nincs arra idő, hogy az egészet elölről kezdjük, és rendesen megtervezzük.

A tervezési hibákat pedig utólag szinte lehetetlen kiküszöbölni - a project már készen  - rossz esetben már a piacon! - van.

 

El tudjuk fedni a hibákat, tovább foltozgatjuk, sőt, megfeszített munkával stabilizálhatjuk az állapotát, és akár biztonságos, jól működő szoftvert állíthatunk elő a kusza kódtömegből. Azonban a mélyben rejtőzködő gonosz előbb-utóbb lehetetlenné teszi a munkát: akkor ütközünk falaknak, ha a termékbe új funkcionalitásokat kell építeni.

 

Tehát a jó szoftver készítéséhez elsősorban jó terv kell.

 

 

Kódolni csak szépen…

 

Ha pedig már hozzákezdtünk a kód írásához, akkor csináljuk szépen. A stílus és a forma itt is nagyon fontos: ne felejtsük el, minden programunk végülis egy szellemi alkotás.

 

Bárki bármit mondjon, a csúnya kódra nincs mentség!

 

Néhány kódolási jótanács, javaslat:

 

1.)    Formázzuk a kódot!

A tiszta kód nem csak esztétikus, könnyebben átlátható, hanem ezáltal könnyebben javítható, bővíthető is egyben. Találkoztam már olyan “profi” kóddal is, mely teljesen balra volt “igazítva”…

Számos – már jól bevált - trükk létezik a kód olvashatóbbá tételére, erre kitérek a “Forma? 1-es…” c. részben

 

2.)     Ne írjunk hosszú, több száz soros függvényeket!

Ha egy függvény nagyon hosszú, azt biztosan al-függvényekre lehet tagolni, az ismétlődő részleteket külön függvénybe foglalni (egy függvényhívás elhanyagolható lassulást eredményez). A “spagetti-kód” szindróma olyan, mint az elszakíthatatlan toalett-papír :°). Néhány tipp a “Spagetti? Kösz, nem... c. részben.

 

3.)    Használjunk egységes jelölést!

Bár sokan idegenkednek a magyar jelöléstől (bár nekünk nem kellene :°)), használata igencsak hasznosnak bizonyulhat egy több ezer soros forráskód esetében. A szoftvercégeknél gyakorlat, hogy használnak valamilyen szintű jelölést a kódban, sőt, általában ezt a minőségpolitikájuk is megköveteli – többet erről a  Nomen est omen” fejezetben találtok.

 

4.)    Nagyon hasznosnak bizonyulhatnak a függvények, metódusok elé helyezett fejlécek ( ezek sima „comment“ mezők, táblázatba foglalva) melyben a függvény rövid leírása, visszatérési értéke stb. található – bővebben a “Használati utasítás” c. fejezetben.

 

5.) Zárjuk TRY-CATCH blokkba a kritikus kódrészleteket, írjunk hibakezelőket, melyek elcsípik a csúnya hibákat, még mielőtt azok az operációs rendszerig gyűrűznének. A témát a “Ha elsőre nem sikerül…“ c. részben fejtem ki bővebben.

 

Ezek együtt átláthatóbbá teszik a forrást, könnyebbé teszik a nyomozást, a hibák okának kiderítését.

 

Ha már kész kódot kapunk kézhez, akkor is érdemes utólagos plasztikázni - bár sok időt vehet igénybe, de a befektetés hosszabb távon mindenképp megtérül. A tiszta kód nem csak esztétikus, de egyben előfeltétele is annak, hogy utólag jól karbantartható legyen.

 

 

Forma? 1-es…

 

Biztos találkoztatok már rosszul, vagy egyáltalán nem  formázott kóddal. Amíg nem kell hozzányúlni, nincs semmi baj, de ha bele kell nyúlnod, olyan érzés, mintha kukáznál (bár utóbbit még nem próbáltam, de nagyon empatikus alkat vagyok).

 

Eligazodni egy ilyen kódban majdnem annyira könnyű, mint egy memory dump-ban. Kivétel persze Neo és barátai…;)

 

Szerencsére a mai 4GL-ek szövegszerkesztői automatikusan rendezik a kódot, főleg, ha jók a beállításaink - nem árt a körmükre nézni!

 

Néhány formázási jótanács (az egyszerűség kedvéért C-ben mutatom be a példákat, de az adott nyelv sajátosságainak figyelembe vételével alkalmazható bármelyik nyelv esetében, az Assembly-től a Delphi-ig):

 

1.)    a blokkok belsejében lévő kódot igazítsuk a blokk szélétől egy TAB-bal jobbra.

Megjegyzem, a TAB-ot illik 4 szóköz méretűre állítani, ha az alapból másra lenne állítva. Ezt a beállítást általában az “Options” illetve “Tools/Options” menüben érdemes keresgélni (szerkesztő-függő).

 

{

i++;

cout << i;

}

 

2.)    Elágazásak esetében nyissunk új blokkot még akkor is, ha csak egy utasítás következik:

 

if ( myEmptyFlag )

{

   findNextItem();

}

else

{

   anItemFoundFlag = true;

}

 

switch( event_in )

{

case NORMAL_COMPLETION:

{

      exit();

} break;

 

case JOBS_IN_QUEUE:

{

      processNextJob();

} break;

 

default:

{

      doNothing();

}

}

 

3.) Minden elágazás, fontosabb utasítás illetve kódrész előtt érdemes kihagyni egy-egy sort a kód jobb átláthatósság érdekében. A kód így “levegősebb” összhatást kelt.

 

Függvények közt akár 2-3 sort is ajánlott kihagyni. Ez is - akárcsak a többi – természetesen csak ajánlás.

 

4.) Bár egyesek szerint jól néz ki, lehetőleg ugyanazon forrásfájlon belül ne keverjük a programozási nyelveket.

Ha feltétlenül szükséges (pl. Assembly kód beszúrása ) az “idegen” rész külön függvényként kerüljön át egy másik állományba, és ezt hívjuk meg. Amennyiben a “gazda”-nyelv támogatja, deklaráljuk az idegen nyelvben írt kódot inline függvényként, így gyakorlatilag nem számít függvényhívásnak, mégsem lesz öszvér-kódunk.

 

 

 

Spagetti? Kösz, nem...

 

Találkoztatok már olyan függvénnyel, amelyiknek se vége, se hossza? Ilyenek olyankor keletkeznek, amikor:

-         a programozó még nagyon kezdő, nincs tisztában a függvényekkel, és csak írja és írja a kódot

 

-         a “fejlesztő“ idegen forrásból, copy-paste módszerrel dobálja össze a programot, és még arra sem veszi a fáradságot, hogy ellenőrizze, mely részek szükségtelenek

-         az adott függvény a program valamelyik fő ciklusa, esetleg olyan függvény, mely túl sok helyről van hívva, és számos bemenő paramétert kell elemezzen

 

-         a programot folyamatosan karbantartják, és nincs sem igény, sem pedig idő arra, hogy rendbe tegyék a kódot (szomorú, de sokszor ez a helyzet - sőt, az is előfordul, hogy a hiba kijavításán kívül semmihez sem szabad hozzányúlni, aminek nincs közvetlenül köze a hibához)

 

Az ilyen véget nem érő függvények komoly fejtörést okoznak a programozóknak. Egyszerűen fárasztó végiggörgetni több képernyőn keresztül, egy változó értékének változását követve, debug-golni meg maga a pokol.

 

Az ilyen hosszú kódblokkoknak nincs létjogosultságuk: szét kell bontani kisebb kódrészletekre – melyek valószínűleg úgyis ismétlődnek – és azokat külön függvényekbe helyezve hívogatni.

 

Kerüljük a bonyolult elágazási feltételeket, melyeket nem lehet egykönnyen átlátni.

Íme egy gyöngyszem:

 

if ( (cmd.Compare(L"IMP") == 0) || (cmd.Compare(L"AR_CD") == 0)

|| (cmd.Compare(L"AR_CD") == 0)||cmd.Compare(L"ARCH_A")==0)|| (cmd.Compare(L"SEND_PREF") == 0)||(cmd.Compare(L"SEND_1") == 0)|| (cmd.Compare(L"SEND_2") == 0)|| (cmd.Compare(L"SELECT_A") == 0)|| (cmd.Compare(L"SELECT_D") ==0)||(cmd.Compare(L"SELECT_EXP")==0)|| (cmd.Compare(L"EJECT")==0)||(cmd.Compare(L"EJECT_CD")==0)
||(cmd.Compare(L"EJECT_CD")== 0)|| (cmd.Compare(L"SEL_EJECT") == 0)|| (cmd.Compare(L"REC_CD") == 0)|| (cmd.Compare(L"FORMAT") == 0)|| (cmd.Compare(L"ARC_C") == 0)|| (cmd.Compare(L"NETW_C") == 0)|| (cmd.Compare(L"IMP_FS")==0)|| (cmd.Compare(L"EXP_FS") == 0))

{

      return true;

}

else

{

      return false;

}

 

Gyönyörű, nem?

Ez a rész gyakorlatilag azt hivatott ellenőrizni, hogy egy ismert értékkel van dolgunk, vagy sem.

 

A fenti példa esetében például ajánlottabb lett volna egy switch-case konstrukciót bevetni, és a default ágban lekezelni az ismeretlen értéket. Persze lehet, hogy az „ellenség“ (értsd kollégák :°)) megtévesztése volt a cél.

 

Ha végleg elkerülhetetlenek a méretesebb feltételek, és a tetejébe még eltérő feltételeket is tartalmaznak, próbáljuk egyértelművé tenni a kifejezések kiértékelési sorrendjét. Lássunk egy példát C-ben:

 

if ( !anEmptyFlag || myCurrentPos > MAX_VAL && aCurPos == MIN_VAL )

{

  

}

 

valamivel könnyebb átlátni, ha “bevetjük” a zárójeleket illetve a sortörést:

 

if ( !anEmptyFlag

|| ( ( myCurrentPos > MAX_VAL ) && ( aCurPos == MIN_VAL ) ) )

{

  

}

 

Az az igazság, hogy számomra mindig gyanús, ha egy feltétel, függvény, kódrészlet túl hosszú. Szinte biztos, hogy szét lehet „darabolni“, vagy sokkal elegánsabban, ésszerűbben is meg lehet írni.

 

 

Nomen est omen

 

A következőkben néhány jótanács, melyek segíthetnek a programban használt változók, osztályok, paraméretek típusának, hatókörének egyértelmű beazonosítására.

 

Felhívnám a figyelmeteket még egyszer: alakítsátok ki az igényeiteknek megfelelő jelölésrendszert - hacsak céges szinten nincs már eleve leszabályozva a névkonvenció.

 

A következőkben néhány főbb – megfontolandó - irányelvet szeretnék ajánlani. Bár C++-ban mutatom be, többségük alkalmazható bármely nyelv esetében, az adott sajátosságok figyelembe vételével.

 

Különböztessük meg a globálisan elérhető változókat a helyi változóktól. Miért is?

 

int x( int x )

{ 

   x++;

   return x;

}

 

int x()

{ 

   int a = 5; // lokálisan deklarált változó, hatásköre megszűnik, amint kilépünk a függvényből

   return x( a );

}

 

int y = 8; // ezt a változó globálisan lett deklarálva, minden függvény "látja"

 

int main()

{

   int y = 7;

   cout << x() << "\n";          // 1.

   cout << x( x() ) << "\n";     // 2.

   cout << x( y ) << "\n";       // 3.

   cout << x( ::y ) << "\n";     // 4.

 

   return 0;

}

 

1.) a paraméter nélküli x() lett meghívva

2.) paraméteres x( int ) lett meghívva, az átadott érték pedig a paraméter nélküli x()függvény visszatérési érteke

3.) paraméteres x( int ) lett meghívva,  a lokális <y> változóval paraméterként

4.) paraméteres x( int ) lett meghívva,  a globális <y> változóval paraméterként (a "::" határozza meg, hogy a globálisan deklarált <y> változóról van szó)

 

Az eredmény:

6

7

8

9

 

de hogy néz ki ez a kód?!

Ilyen kódot (remélhetőleg) senki sem ír, csak próbálom érzékeltetni, mennyire veszélyessé és átláthatatlanná teszi a kódot valamilyen jelölésrendszer hiánya.

 

Átírva a kódot egy jól bevált jelölésrendszerrel rögtön másképp néz ki:

 

int inc( int int_in )

{ 

   int_in++;

   return int_in;

}

 

int func()

{ 

   int anInt = 5; // lokálisan deklarált változó,

   return inc( anInt );

}

 

int myInt = 8; // globálisan deklarált változó, bárhonnan elérhető

 

int main()

{

   int anInt = 7;

   cout << func() << "\n";

   cout << inc( func() ) << "\n";

   cout << inc( anInt ) << "\n";

   cout << inc( myInt ) << "\n";

   return 0;

}

 

A globális változót (myInt) “my” előtaggal különböztettem meg a lokális változóktól, melyek viszont az “a”, “an” előtagot kapják. A változók típusát is megjelölöm a nevükben:

 

Típus          Globális                Lokális

 

bool           bool myBusyFlag;        bool anEmptyFlag;

char           char myCharBuffer;      char aCharArray[];

stb.

 

A szóköz használata (szerencsére) nem megengedett – elválasztó jelnek számít - ezért, ha több szóból áll a változónév, az új szó nagybetűvel kezdődik (ezt a módszert SMS-írásnál is használom, egyfajta tömörítés – perszeAzOlvashatóságRóvására,bárIdővelMegLehetSzokni :°) ).

 

A függvények kisbetűvel kezdődnek, mivel nagybetűvel az osztályneveket szoktam kezdeni:

 

char* fillBuffer();

class RoundedRectangle;

 

A függvények bemenő paraméterei kisbetűvel kezdődnek, és “_in” utótagot kapnak, a kimenők pedig “_out”-ot, ha pedig bemenő és kimenő is, értelemszerűen “in_out”-ot:

 

bool findValue( int value_in, int& value_out );

 

Bárhogyan alkalmazzátok a jelölést, a puszta léte garantálja, hogy az általatok írt kód tisztább, rendezettebb lesz. Legyen az egy tanár, egy kolléga, esetleg a megrendelő, aki belenéz a munkátokba, mindenképp jó benyomást fogtok kelteni.

 

A következő két részlet magáért beszél:

a)

#define MAX_VALUE = 5

 

int aCounter;

bool anEmptyStringFlag = false;

CString anEmployeeName;

 

for ( aCounter = 0; aCounter < MAX_VALUE; aCounter++ )

{

if ( anEmployeeName.isEmpty() )

{

      anEmptyStringFlag = true;

break;

}

}

 

if ( anEmptyStringFlag && aCounter < MAXVALUE )

{

   cout << “Database damaged!” << “\n”;

}

 

b)

int i;

bool k = false;

CString j;

for ( i = 0; i < 5; i++ )

{

if ( j.isEmpty() )

{

k = true;

      break;

}

}

 

if ( k && i < 5 )

{

         cout << “Database damaged!” << “\n”;

}

 

Nos, melyik kódrészlet tetszik jobban? És vajon melyiket lenne könnyebb karbantartani? (Ha a válaszod :”Egyik sem!”, akkor sincs harag – csak azt ne mondd, kérlek, hogy a b) változat jobban tetszik J  )

 

Használati utasítás

 

Nem mindig a legszórakoztatóbb utólag megfejteni, mit is csinál egy adott kódrészlet vagy függvény. Ezért aztán nem kell szűkölködni a magyarázatokkal, kommentezéssel, hiszen a kódolás pillanatában rálátunk arra, amit csinálunk, viszont lehet, hogy 6 hónap múlva már nem. Arról nem is beszélve, ha csapatmunkáról  van szó, és bele kell “túrni” egy olyan osztályba, függvénybe, amivel előzőleg még nem találkoztál.

 

Nos, ha a kód szellemi atyja fukarkodott a kommentekkel, akkor kezdődhet a nyomozás, ami természetesen idő, az idő meg pénz - ezért a project-vezető “megkér”, hogy gyere be szombaton is dolgozni… és ücsöröghetsz a besötétített irodában, soronként lépkedve a kódban, ahelyett, hogy a strandon süttetnéd a hasad.

 

Természetesen nem kell túlzásba vinni a dolgot, értelemszerűen nem fogunk minden mezei változó deklarációt elmagyarázni.

 

A függvények esetében jól jöhet egy egységes fejléc használata, melybe néhány fontos adat, leírás kerülhet be.

 

/*----------------------------------------------

Név                  : isPrime

Leírás               : a függvény a paraméterként megadott számról   megállapítja, hogy prím-e.

Visszatérési érték   : bool

stb

*/----------------------------------------------

bool isPrime( int number_in )

{

}

 

Ez barátságosabbá teszi a kódunkat, hisz első ránézésre meg lehet állapítani, hogy az adott függvény mi célt szolgál.

 

Fűzzünk magyarázatokat a bonyolúltabb kódrészletekhez, egzotikus típusokhoz (azok használatával kapcsolatos tudnivalók, kivételek), bizonyos függvényhívások mellé (elé).

 

Jóindulatról tesz tanúbizonyságot, ha “feltárjuk” fejlesztőkollégáink előtt felfedezéseinket, furcsa hibákkal kapcsolatban figyelmeztetéseket szúrhatunk a kódba, hogy más ne essen ugyanabba a csapdába, mint mi.

 

A magyarázatokat természetesen ésszerűen kell bevetni, a terjengősebb okfejtéseket, elemzéseket külön dokumentumba helyezni (és erre utalni a kódban).

 

Ha elsőre nem sikerül…

 

Soha nem lehetünk teljesen biztosak abban, hogy a látszatra hibátlan kódunk valóban hibátlan. Előfordulhat, hogy rajtunk kivül álló okok miatt „száll el“ a programunk (például egy külső függvény hívása miatt). Hiába nézegetjük a kódot, minden szép, tiszta, rendezett, logikus – de valahogy mégis lefagy a programunk – teljesen véletlenszerűen...

 

Ilyenkor bizony jól jönne, ha tudnánk, mi is volt a probléma oka.

 

Erre találták ki a trycatch párost, melyet megtalálunk bármely korszerűbb programozási nyelvben. Szerepük: elcsípni a hibát. Mindig abból induljunk ki, hogy ami elromolhat, az el is romlik. Ezért aztán próbálkozzunk (try). Ha nem jön össze, dobjunk ki (throw)valami hibafélét, és kapjuk el (“catch) a hibát. Mindez C++-ban megvalósítva:

 

try
{
 
  ToProgram();
   if(Error) throw Monitor;
}


catch(Monitor)
{
   BringMonitorBack();
}

 

Na jó, akkor most komolyabban :°) :

 

int fillData()

{

MyPointer* aPointer;

try

{

aPointer = new MyPointer;

 

if( !aPointer )

{

throw "Nem sikerült memóriát lefoglalni!";

}

}

 

catch( char * str )

{

cout << "Kivétel: " << str << '\n';

logError( str );

}

 

delete aPointer; // felszabadíjuk a lefoglalt területet

aPointer = NULL;

// ...
 
return 0;

}

 

Ha a hibát egy - a hibákat naplózó - állományba mentjük, már be is mértük, melyik függvényünkben – vagy akár melyik sorban! - történt a baj – és ez óriási segítség lehet egy több száz kbyte forráskód esetében…

 

Minden függvény esetében ellenőrizzük a visszatérési értékét, illetve kérdezzük le a hibakódot, amit a függvény visszatérésekor kapunk (Windows esetében GetLastError() ).

 

Ütőszó (nem elírás :) )

 

A szép kód, és a fokozott elővigyázatosság sem garantálja a hibátlan működést és jó teljesítményt, de kétségtelenül előfeltétele ezeknek. Előfordulhat, hogy az általunk hívott API függvényben van a hiba, vagy egy dokumentálatlan jelenség okozza az „elszállást“. Tény, hogy egy ilyen „érdekes“ hibát is könnyebb megtalálni, ha nem a kód kuszaságával kell előbb megküzdenünk.

 

Másrészt, manapság a programozók ritkán dolgoznak magányosan egy projecten, így szinte biztos, hogy az általunk írt kóddal hamarosan valaki más is találkozni fog.

 

Miért kerüljünk zavarba, amikor rájövünk, hogy mi magunk sem tudjuk, mit miért írtunk úgy, illetve pontosan mit is csinál a függvényünk? Ha jól dolgoztunk, értetlenül a kolléga szemébe nézhetünk, és ártatlan képpel megkérdezhetjük:

„-Elolvastad a fejlécben található leírást? Akkor figyelj, elmagyarázom egy perc alatt.”

 

Legyünk igényesek, dolgozzunk szépen, tisztán. Kódoljunk, és ne hekkeljünk!

 

A Kód legyen Veletek!

 

Nyisztor Károly Levente - nyisztor.karoly@evosoft.hu