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 try – catch
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