2. fejezet Strukturált programozás 2.1.
Bevezetés
A strukturált programozás jelenti valamennyi ma használatos programtervezési módszer alapját. Ebben a fejezetben a strukturált programozással kapcsolatos alapokat és gyakorlati kérdéseket tekintjük át. Olyan kérdésekre akarunk válaszolni, mint például hogy mi a strukturált programozás lényege? Mi a szerepe a goto parancsnak? Miért érdemes bizonyos vezérlési szerkezeteket gyakrabban használni? A strukturált programozás ma már szerves része valamennyi programtervezési módszernek. Napjainkban már széles körben elfogadott az a nézet, hogy a strukturált programozás a programfejlesztés nemcsak hasznos, hanem gyakorlatilag egyetlen lehetséges módja. Ugyanakkor nem volt ez mindig ennyire egyértelmû. Nem is oly régen heves vita folyt arról, hogy használható-e valamire ez a megközelítési mód, és hogy egyáltalán jó ötlet volt-e felvetni a használatát. A strukturált programozás egyik alapelve szerint a programok összesen háromféle építõelembõl állnak össze: vannak utasítássorok (ezek tagjait a végrehajtás sorrendjében adjuk meg), vannak döntési szerkezetek (if
then
else szerkezetek) és vannak ismétlõdõ ré-
12
Programtervezés
szek (while
do ciklusok). Mint látható, ebbõl a szemléletbõl a goto utasítás teljesen hiányzik, ennek használata kerülendõ. Ebben a fejezetben hamarosan megvizsgáljuk majd a goto mellett és ellen szóló érveket. Már itt elárulhatjuk a végeredményt: a goto utasítás tulajdonképpen szükségtelen. A lényeg az, hogy a programnak jól átgondolt szerkezete legyen. A késõbbiekben megvizsgáljuk a strukturált programozás alapelemeit. Érintünk ezen kívül néhány kiegészítõ kérdést is. Ha a strukturált programozás három építõelemét folyamatábrán szemléltetjük (2.1. ábra), rögtön nyilvánvalóvá válik ennek a tervezési módszernek néhány alapvetõ tulajdonsága: 1. 2.
Minden építõelemnek csak egy belépési pontja van. Egyetlen programelem sem tartalmaz háromnál több alapelemet.
Ha a három alapelemet úgy ábrázoljuk, ahogyan a gyakorlatban használjuk õket, egy harmadik alaptulajdonság is felismerhetõvé válik: 3.
A belépési pont a program elején, a kilépési pont pedig a végén található.
2.1 ábra De miért is olyan fontosak ezek a tulajdonságok? Miért zárjuk ki más, egyébként ugyanilyen tulajdonságokkal rendelkezõ szerkezetek (2.2. ábra) használhatóságát? Ebben a fejezetben ezekre a kérdésekre fogunk választ adni.
Strukturált programozás
2.2 ábra
2.2.
Miért ne használjuk a goto parancsot?
A goto parancs használata fölösleges A Bohm és Jacopini (1966) által bebizonyított tétel szerint a goto parancs segítségével megírt bármely program megírható pusztán a strukturált programozás alapelemeinek használatával is. Az így átalakított programban valószínûleg szükség lesz néhány új, a döntési szerkezetekben címkeként vagy jelzõként használatos változó bevezetésére. Valójában az ilyen programok néha kissé mesterkéltnek tûnhetnek, de maga az átalakítás mindig elvégezhetõ. Ennek megfelelõen pusztán elvi síkon nincs szükség arra, hogy bármely programunkban a goto parancsot használjuk. Érdemes talán még megjegyezni, hogy a fent említett tétel arra nézve semmit sem állít, hogy a strukturálatlan programot miként alakítsuk strukturálttá. Csupán azt mutatja meg, hogy ez az átalakítás elvégezhetõ.
Kísérleti tapasztalatok A strukturált programozás mint tervezési és megvalósítási módszer jól megalapozott, és általánosan elfogadott. Ezek alapján logikusnak
13
14
Programtervezés
tûnhet az a feltételezés, hogy a gyakorlat immár egyértelmûen bebizonyította a módszer használhatóságát. Ez azonban nem igaz. Gyakorlatilag egyáltalán nem léteznek valós problémák megoldásán alapuló bizonyítékok a strukturált programozás mellett. Ennek pedig javarészt az az oka, hogy egy ilyen kísérlet végrehajtása igen körülményes és drága lenne. Kétszer kellene megíratni egy nagy programrendszert programozók két csoportjával. Egyszer végig a strukturált programozás alapelveit kellene használni, egyszer pedig a strukturálatlan elveket. Természetesen mindkét munkafolyamatnak megfelelõ központi irányítással kellene rendelkeznie. Ezek után a két elkészült programot össze kellene hasonlítani bizonyos gyakorlati szempontok alapján. Ilyen szempont lehetne a program megírására fordított idõ, vagy a felfedezett hibák száma. Sajnálatos módon egyetlen ilyen összehasonlítás sem készült a mai napig. Ugyanakkor léteznek olyan, kisebb feladatokkal kapcsolatos összehasonlító tanulmányok, amelyek során azt vizsgálták, hogy a témában járatos programozók mennyire könnyen értenek meg egy forráskódot, ha az strukturált vagy ha strukturálatlan. Egy csoport minden tagjának megmutatták egy strukturált program forráskódját, majd a programozóknak olyan kérdésekre kellet válaszolniuk, amelyek alapján felmérhetõ volt, milyen mélységig látták át a program szerkezetét. Mérték mind a válaszok pontosságát, mind a válaszadáshoz szükséges idõt. Ez a két alapvetõ jellemzõ azt mutatja meg, hogy az adott programot egy programozó mennyire gyorsan tudja karbantartani, vagy mennyire könnyû a kód nyomkövetése. Egy másik csoport tagjai ugyanennek a programnak a strukturálatlan átiratát kapták meg. Végül összehasonlították a két csoport válaszidõit, illetve a válaszok pontosságát. Valamennyi ilyen vizsgálat azt bizonyította, hogy a strukturált programok sokkal jobban idomulnak az említett gyakorlatias igényekhez. A fejezet végén felsoroltuk azon szakcikkek bibliográfiai adatait, amelyek e téma áttekintésével foglalkoztak. Egy ilyen, 1984-ben megjelent áttekintõ cikkben jóval azután, hogy a strukturált programozás körüli csaták zaja elcsendesedett Vessey és Weber arra a következtetésre jutott, hogy a strukturált programozás használhatóságát alátámasztó érvek meglehetõsen gyengék. Ennek a velõs megállapításnak hátterében leginkább az áll, hogy nehéz valóban megbízható kísérleteket tervezni és végrehajtani.
Strukturált programozás
Világos írásmód és kifejezõerõ Hasonlítsuk össze a következõ, egymásnak mindenben megfelelõ programrészleteket:
label:
--------------------if a > 0 goto label -----------
----------while a > 0 do ----------endwhile -----------
Ha az elsõ kódot felülrõl lefelé haladva olvassuk, nem világos azonnal, hogy mi a szerepe a label nevû címkének és a goto parancsnak. Egy valós helyzetben valószínûleg eltart majd egy ideig, mire rájövünk, hogy ezek tulajdonképpen egy ismétlõdõ kódrészlet megvalósítására szolgálnak. Ugyanez a második program while parancsát olvasva azonnal nyilvánvaló. És ami még ennél is rosszabb, az elsõ esetben soha nem lehetünk teljesen biztosak benne, hogy nincs valahol a programban még egy goto parancs, ami ugyanerre a címkére ugrik. Egy programozási nyelv elemeinek lehetõvé kell tenniük, hogy a programozó világos módon írhassa le, amit meg akar valósítani. Ha megvizsgálunk egy goto parancsokat is tartalmazó programot, rendszerint azt fogjuk tapasztalni, hogy a goto több, egymástól teljesen különbözõ cél megvalósítására is használható. Lássunk ezek közül néhányat:
Kikerülhetünk vele egy kódrészletet (amit más körülmények között kell végrehajtani). Megvalósíthatunk vele ismétlõdést. Kiléphetünk vele egy ciklusból. Meghívhatunk vele egy megosztott kódrészletet.
Ha egy kódban látunk egy goto parancsot, a környezete általában meglehetõsen kevés információt szolgáltat arról, mi is volt vele a programozó szándéka. A megoldás természetesen az, ha minden
15
16
Programtervezés
helyzetben a megfelelõ egyedi és egyértelmû nyelvi elemet használjuk. A fenti felsorolásnak megfelelõ sorrendben ezek az egyedi elemek a következõk: if
then
else while
do vagy repeat
until exit
eljáráshívás
A goto parancs tehát olyan, mint egy egészen egyszerû gépi parancs például egy adott regiszter feltöltésére szolgáló gépi utasítás , ami számos egészen különbözõ helyzetben használható, de egyik esetben sem derül ki egyértelmûen a használat célja. Összefoglalva tehát a goto-nak kicsi a kifejezõereje, így nehéz megérteni az olyan programok belsõ logikáját, amelyek erre támaszkodnak. Ha olvasunk egy forráskódot, és olyan utasításokat látunk, mint a while vagy az if, akkor egészen határozott elképzelésünk van az adott helyzetrõl. Ha egy goto-t látunk, legfeljebb találgatni tudunk.
Hány ceruza? Tegyük fel, hogy meg akarjuk érteni egy program mûködését, ezért az elejétõl kezdve elkezdjük úgy olvasni, ahogy a számítógép teszi, és gondolatban végrehajtjuk az utasításokat. Tegyük fel továbbá, hogy van néhány ceruzánk (vagy ujjunk), amelyek segítségünkre lehetnek a dolgok követésében. A ceruzákat jelzõként fogjuk elhelyezni a kódszöveg azon pontjain, amelyek valamiért érdekesek lehetnek. Ha egy egyszerû utasítássort hajtunk végre, mindössze egy ceruzára lesz szükségünk ahhoz, hogy mindig pontosan tudjuk, hol tartunk. Ha belefutunk egy eljáráshívásba, már két ceruza kell. Az egyiket a meghívás helyén kell hagynunk, hogy tudjuk, hová kell majd visszatérni, a másikkal pedig az eljárás utasításait fogjuk követni. Ha egy while vagy for utasítást látunk, szükségünk lesz egy számlálóra is, amellyel azt tartjuk számon, hogy hányszor hajtottuk végre a ciklusmagot.
Strukturált programozás
Összefoglalva tehát, ha strukturált programmal van dolgunk, akkor kell: 1. 2. 3.
egy ceruza az aktuális hely számontartása végett; egy-egy ceruza minden olyan eljáráshíváshoz, amelybõl a végrehajtás még nem tért vissza; egy-egy számláló minden egyes még be nem fejezett ciklushoz.
Ez így elsõre meglehetõsen sok kiegészítõ felszerelésnek tûnhet, de gondoljuk csak meg, mi történne, ha mindezt goto parancsokkal valósítanánk meg. Akárcsak az elõbb, ekkor is szükségünk lenne egy ceruzára az aktuális utasítás nyilvántartásához. Szükségünk lenne aztán minden egyes végrehajtott goto parancs jelzésére. Míg azonban a strukturált programok esetében elvehetünk egy ceruzát, valahányszor visszatértünk egy eljárásból vagy befejeztünk egy ciklust, illetve döntési szerkezetet, most a ceruzák száma soha nem csökken, sõt egyre többre lesz szükség. A ceruzák növekvõ száma pedig egyértelmûen jelzi a goto paranccsal megvalósított szerkezetek összetettségét. Az igazi probléma akkor válik nyilvánvalóvá, ha megpróbálunk visszaemlékezni arra, hol jártunk, mielõtt egy adott ponthoz érkeztünk. Ha nincs goto parancs, akkor egyszerûen visszalapozunk a kódszövegben. A strukturálatlan program esetében csak addig lehetünk magabiztosak, amíg a visszagörgetés során el nem érkezünk az elsõ címkéhez. Itt ugyanis semmilyen módon nem fogjuk tudni megállapítani, honnan és hogyan kerültünk a program kérdéses pontjára.
Az olvasás megkönnyítése (statikus és dinamikus szerkezetek) Mi itt a mûvelt Nyugaton alaposan hozzászoktunk ahhoz, hogy az írott szövegeket balról jobbra és felülrõl lefelé haladva olvassuk. Ennek megfelelõen nagyon kényelmetlennek és természetellenesnek érezzük, ha egy kódszöveget elõbb felülrõl lefelé kell olvasni, majd idõnként visszaugrani benne. Sokkal egyszerûbb a dolgokat mindig a megadás sorrendjében olvasni. Ez pedig éppen a strukturált programok egyik nagy elõnye. Ezeket mindig végig lehet olvasni felülrõl
17
18
Programtervezés
lefelé haladva, feltéve persze, hogy nem tartalmaznak eljáráshívásokat. Az egyetlen kivétel a while ciklus, ahol az ismétlõdõ kódrészlet elején található ciklusfeltételt minden végrehajtás után meg kell vizsgálnunk. A programok mûködése szükségszerûen dinamikus, hiszen valamennyien egy-egy kívülrõl szabályozott folyamatnak tekinthetõk, a program forráskódja azonban statikus szöveg. Ha meg akarjuk könnyíteni a kód megértését, e két ellentétes dolog között kell valahogyan összhangot teremtenünk. A statikus szövegnek szerkezetével tükröznie kell az általa leírt folyamatokat. A strukturált programok esetében a részfolyamatok sorrendje közelítõleg mindig megfelel a programsorok írási sorrendjének, ami lehetõvé teszi, hogy a kódot felülrõl lefelé, vagyis a lehetõ legtermészetesebb módon olvassuk.
A programok helyességének bizonyítása Bebizonyítani azt, hogy formailag valamennyi programunk helyes, a ma technikájával egyszerûen lehetetlen. Ugyanakkor van néhány olyan dolog, ami nem árt tudni a programok helyességének bizonyításával kapcsolatban. A bizonyítás egyik módja az, hogy néhány stratégiai fontossággal bíró ponton ellenõrizzük bizonyos logikai feltételek teljesülését. Az ilyen ellenõrzésnek mindig az a lényege, hogy a program által kezelt adatokkal kapcsolatban megfogalmazunk olyan logikai feltéteket, amelyeknek mindig igaznak kell lenniük. Ha az ellenõrzést a program elején és végén hajtjuk végre, akkor bemeneti feltételrõl és kimeneti feltételrõl beszélünk. A program helyességének bizonyítása tulajdonképpen azt jelenti, hogy matematikai szigorúsággal megmutatjuk, hogy ha programunk bemenete helyes, akkor a kimenete is mindig helyes lesz. A strukturált programok csupa olyan elembõl állnak, amelyeknek egyetlen belépési és egyetlen kilépési pontja van. Ez önmagában is jelentõsen megkönnyíti a program mûködésével kapcsolatos bizonyítások elvégzését. Ugyanakkor, ha a program goto utasításokat is
Strukturált programozás
tartalmaz, gyakran lehetetlen világosan elkülöníteni benne a szerepükben független, egy bemenettel és egy kimenettel rendelkezõ modulokat. Még ha nem is használunk formális módszereket a helyesség bizonyítására, csupán egyszerû szemrevételezéssel próbáljuk eldönteni, hogy programunk jól vagy rosszul mûködik, a logikai rendszer átlátását akkor is nagyban elõsegíti, ha az említett egyszerû tulajdonsággal rendelkezõ elemekbõl építkezünk.
2.3.
Miért használjuk mégis a goto parancsot?
A szakértelem csorbítása Akárhogy is nézzük, a goto parancs is része a programozási nyelv eszközkészletének. Ha egyszerûen megtiltjuk a használatát, azzal a programozó kezébõl kivesszük ezt az eszközt, és abban is megakadályozzuk, hogy a megfelelõ helyzetekben értelmes módon felhasználja. Képzeljünk el egy kézmûvest, akinek az a specialitása, hogy fából különbözõ használati tárgyakat készít. Tegyük fel, hogy ettõl az embertõl elveszünk egy olyan eszközt például egy baltát amirõl úgy gondoljuk, hogy nincs is igazán szüksége rá. Ezzel nyilván lecsökkentettük a választási lehetõségeit, és azt a képességét is fölöslegessé tettük, amely a balta használatához szükséges. (Ugyanakkor lehetnek olyan helyzetek, amikor a legalkalmasabb munkaeszköz éppen a balta lenne.)
Kivételek Viszonylag gyakran bukkan fel a programozás során az a követelmény, hogy a kész program képes legyen bizonyos hibák felismerésére és kezelésére. Tegyük fel, hogy egy ilyen kezelendõ hiba az eljáráshívások sokadik mélységi szintjén következik be. A hibakezelés egyik lehetséges módja az, hogy valamennyi eljáráshívásnál meg-
19
20
Programtervezés
adunk még egy bemenõ paramétert. Ez az eljárás persze igencsak kényelmetlenné válhat, hiszen minden eljárásnál kínosan ügyelnünk kell arra, hogy átadjuk neki azt a paramétert, amit valószínûleg nem is fog semmire használni. Sokkal jobb megoldásnak tûnik, ha egy goto paranccsal bármely eljárásból a program egy adott helyére ugorhatunk, ahol aztán központilag kezelhetjük a hibát. Ez nyilvánvalóan a programszerkezet lényeges egyszerûsödését eredményezi. A megoldás lényege tulajdonképpen az, hogy egy kivételes helyzet kezelésére egy kivételes megoldást használunk. Persze egyes programnyelvek ilyen például az Ada ezt a problémát eleve megoldják azzal, hogy a kivételkezeléshez külön eszközöket biztosítanak.
A programok teljesítmény e Egyes esetekben egy goto parancsok segítségével megvalósított program gyorsabban futhat, mint egy olyan, amiben mellõztük a goto használatát. Jó példa erre az az eset, amikor egy a táblázatban egy x értéket kell megkeresnünk: for i:=1 to a_tablazat_merete do if a(i)=x then goto megtalaltuk endif endfor nem_talaltuk_meg:
megtalaltuk:
Ugyanezt egy szép strukturált program formájában a következõképpen lehetne megvalósítani: i:=1 while i