OOP
(Objektum Orientált Programozás Pascal
nyelven)
1. Programok szerkezete
1.1 Lineáris szerkezet
A lineáris
programfelépítés a hagyományos, a programozás kezdetén alkalmazott szerkezet.
Általában rövid programokat írunk vagy írtunk ilyen felépítésben. A program
alapjában véve lineáris lefutású, az első utasítástól az utolsóig történik a
végrehajtás. A program futását esetleg egy inputra való várakozás szakíthatja
meg, vagy a végén, a kimeneti képernyő megtartására írunk végtelen ciklust. Ezt
a programozási stílust figyelhetjük meg a hagyományos BASIC programoknál.
De már a BASIC is
megadta a lehetőséget a továbblépésre azzal, hogy GOSUB - RETURN szerkezeteket
engedett meg. Ennek a lehetőségnek a kihasználásához elengedhetetlen a GOTO
utasítás, mely idegen a strukturált programozástól. A programozó tanulása során
általában kisebb lélegzetű programoktól halad a nagyobbak felé, megtanulva azt,
hogyan kerülheti el azt a softver krízist, amikor a saját programját már csak
nehezen látja át, és csak nagy nehézségek árán tudja továbbfejleszteni. Ezt
segítheti BASIC-ben a változótábla. Kezdetben maximum csak két karakteres
lehetett a változónév, ezért külön tervet kellet készíteni, hogy milyen néven
milyen változókat használunk a programban. A másik a programlista felosztása: a
lista elejére helyezte a szubrutinokat, a végére a főprogramot, amely esetleg
már menüt is tartalmazott. Ez a felépítés már hordozott magában némi
strukturáltságot, de a lista alapvetően még mindig lineáris, hiszen a
szubrutinoknak nincsenek határozott belépési pontjai, így semmilyen zártságot nem
mutat. Maga a BASIC ezt nem támogatja.
A BASIC, és ezáltal a
lineáris programszerkezet legnagyobb hibája az, hogy nem ad igazi lehetőséget a
kód-újrafelhasználásra, amely mint az a továbbiakban látható, a legfontosabb
eszköze a hatékony programozási gyakorlatnak. A fejlődés és fejlesztés legfőbb
mozgatója pedig az, hogy hogyan lehet minél hamarabb minél hatékonyabb és
biztonságosabb programot írni. Ehhez a kód-újrafelhasználás megoldása
elengedhetetlen.
1.2 Moduláris szerkezet
A moduláris programozásban a program által megoldandó
feladatot részekre, úgynevezett modulokra bontjuk szét. Megírjuk a
programvázat, amelyben egy-egy modul egy-egy részfeladat megoldásáért felelős.
Készíthetünk egy főmodult is, amely a modulokat egységbe foglalva kezeli. A PASCAL
nyelv támogatja a moduláris programozást. A modul ebben a nyelvben lehet
eljárás vagy függvény. A két modul nagyon hasonló, lényegében csak abban
különbözik, hogy a függvény visszaadott értékkel bír, és így állhat valamely
kifejezésben, vagy az értékadás jobb oldalán. Mindkét modulnak létezhetnek
paraméterei, mely a modul végrehajtást pontosítják, vagy egyáltalán lehetővé
teszik. A függvény visszaadott értékének típusa csak a következő lehet:
Boolean, Char, String, Byte, Word, ShortInt, Integer, Longint, Real,
Double, Extend és Pointer. Nem lehet
viszont semmilyen összetett, az előzőekből leszármaztatott vagy felhasználói
típus. A PASCAL lényegében eljárás orientált nyelv. Ügyesen megírt moduljainkat
pedig egy másik programban is használhatjuk, egyszerűen csak egységekbe (Unit)
kell elhelyezni őket, és a másik programban az egységet használatba kell venni.
Ez már igazi kód-újrafelhasználás. A moduláris programozás egyik módszere a
fentről lefelé történő kidolgozás, amikor megírjuk az úgynevezett főprogramot,
és a modulok fejeit pedig megfelelő számú egységben. Ha ügyesen szerkesztjük
meg az egészet, akkor a program már az első pillanatoktól futtatható. A modulok
kidolgozása csak ezek után következik úgy, hogy mindig csak egy-egy részt
finomítva haladunk a feladat teljes megoldásáig. A moduláris programozást
gyakran módszeres programozásnak is szokták nevezni. A moduláris programban a
modulok közötti kapcsolatot a változók biztosítják. Ezért a moduláris
programozás egy jól átgondolt adatstruktúrát feltételez, de erre még a
továbbiakban visszatérünk. A Pascal nyelv nem korlátozza az egy program által
használatba vehető egységek (Unit-ok) számát. Ennek gyakorlatilag a gép
memóriája szab határt. Ha ezt a határt is át szeretnénk lépni, akkor lehetőség
van az úgynevezett Overlay technika alkalmazására. Ez gyakorlatilag azt teszi
lehetővé, hogy ne minden Unit kódját kelljen egyszerre a memóriába tölteni,
hanem csak azokat, amelyeket a program éppen használ. Ha később másikra van
szüksége, akkor azt tölti be a használat idejére. Ezt az Overlay menedzser úgy
hajtja végre, hogy először a legnagyobb terjedelmű Unit-oknak foglal helyet,
hogy a később ráírandó legfeljebb csak egy Unit területét írja fölül. Ezzel a
memória töredezettségét minimálisra szoríthatjuk.
1.3 Menüvezérelt program
A menü a számítógépes programban gyakorlatilag az
interfész terület a program és a használója között. A menü minden menüpontja
mögött lényegében egy-egy funkció van, melyet a menü kiválasztásával
aktiválhatunk. Olyan ez mintha parancsokat adnánk a programnak, hogy most kérem
ezt végrehajtani, most pedig azt, vagy egyáltalán - legyen vége a program
futásának. Amikor moduláris programot használunk, akkor gyakorlatilag a
főprogram egy menü futtatását jelenti, melynek menüpontjai mögött egy-egy
kisebb nagyobb modul vagy egység van elhelyezve, megírva. A lineáris programfelépítéssel
szemben nyilvánvaló az előny: egy-egy programrészletet tetszőlegesen sokszor
újra meg újra lejátszhatunk anélkül, hogy kilépnénk a programból a futtató
környezetbe. Természetesen a legtöbb esetben ennek csak akkor van értelme, ha
közben a meghívott modul végrehajtását paraméterekkel megváltoztattuk. Ez nem
más, mint a programon belüli kód-újrafelhasználás.
1.4 Eseményvezérelt program
Miközben egy számítógépes programot futtatunk, számos
hatás érheti a programunkat. De természetesen csak akkor, ha eme hatásokat a
program képes felfogni, és arra ésszerűen reagálni. Ezeket a hatásokat
kiválthatja a felhasználó, vagy a számítógép bármelyik egysége, erőforrása. A
felhasználó leginkább a billentyűzethez vagy az egérhez nyúl a program
használata közben. Esemény lehet a billentyűzeten egy gombnak a megnyomása, az
egér megmozdítása illetve kattintás valamelyik egérgombbal, esetleg ez utóbbi
kettő egyidejűleg. De eseményeket generálhatnak a számítógép egységei: a
nyomtató üzen, hogy milyen az állapota (Pl.: kifogyott a papír), a lemezes
egység, hogy nincs kész az olvasásra, vagy írásvédett a lemez, vagy elfogyott a
memória valamely része. Ha egy program eseményvezérelt, akkor az mindezen
eseményekre valahogy reagálni képes. Az eseményvezérelt programban általában
objektumok vannak, amelyek kvázi független életet élnek, a közös csak annyi
bennük, hogy képesek reagálni az őket ért ingerekre, eseményekre és képesek
egymással kommunikálni. Egy eseményvezérelt program futása közben nyoma sincs a
lineáris végrehajtásnak. A gyakorlatilag véletlenül bekövetkező események
vezérlik a programot, nem lehet tehát azt tudni, hogy éppen melyik objektum
lesz aktív a következő pillanatban.
Egy eseményvezérelt program rendelkezik a következő
képességekkel:
-
események
begyűjtése egy eseménylistába;
-
események
szétszórása az objektumok felé;
-
az objektumok
kezelik az eseményeket, és ha nekik szólt, akkor törlik az eseménylistából;
-
ha egy eseményt
egyetlen objektum sem tudta lekezelni, akkor a főprogramnak kell rá reagálni
vagy hibaüzenettel, vagy figyelmeztetéssel a felhasználó felé, vagy csak
egyszerűen neki kell törölni az eseménylistából; és a legfontosabb képesség:
-
a program a sok
és véletlenül létrejövő esemény hatása ellenére - sőt talán éppen általuk –
működőképes tud maradni.
A napjainkban megírt bármely eseményvezérelt program
szinte biztosan egyúttal objektum orientált is. Nem meglepő tehát, ha már most
eláruljuk, hogy az igazi kód-újrafelhasználás az objektumorientált
programírással valósítható meg. De ahhoz, hogy ezt megértsük, még sok mindent
meg kell tanulnunk. Nézzük tehát, hogyan vezet az út a byte-októl az
objektumokig.
2. Adatstruktúrák
2.1 Adatok típusai
Nem tipizált nyelvek esetén a programban bárhol
használatba vehetünk egy változót. Méghozzá úgy, hogy annak típusát, sőt még
létezését sem írtuk le azt megelőzően. A program a változó használatából dönti
el azt, hogy milyen típusú. Ha számmal töltöttük fel, akkor olyan típusúnak, ha
szöveggel, akkor string-nek (karakterláncnak) gondolja. Nem tipizált nyelvek
esetében általában csak egyféle összetett típus létezik, a tömb. Ennek
alapértelmezett indexhatára legtöbbször 0-10 között van. Ha ennél nagyobb
tömbre van szükségünk, akkor ezt előzőleg Dim prefixum segítségével
dimenzionálni kell. Az újradimenzionálást általában a nyelvek nem engedik meg.
A tömb viszont lehet számok és string-ek tömbje is, természetesen egyszerre
csak az egyik. Ilyen nem tipizált nyelv például a hagyományos Basic, vagy az
Excel táblázatkezelőben megtalálható Visual Basic is.
Tipizált nyelvek esetén, mint amilyen a Pascal
nyelv is, csak előzőleg, azonosítójával és típusával megadott (deklarált)
változót vagy konstanst használhatunk programjainkban. Mivel ez lényegében
kötöttséget jelent, ezért a nyelv kidolgozói maximálisan igyekeztek - a
felhasználók igényeit figyelembe – minél több féle adattípust beépíteni a
nyelvbe, sokkal többet, mint az a nem tipizált nyelveknél megfigyelhetünk. A
Pascal nyelv adattípusainak egyik lehetséges csoportosítása a következő:
-
egyszerű
-
sorszámozott
-
egész (byte,
word, shortint, integer, longint)
-
logikai
-
karakter
-
felsorolt
-
intervallum
-
valós (real, double, extend)
-
karakterlánc
-
strukturált
-
tömb
-
rekord
-
objektum
-
halmaz
-
állomány (szöveges,
nem tipizált, tipizált)
-
mutató
-
típusos
-
típus nélküli
-
eljárás
-
objektum.
Látva a felsorolást
megállapíthatjuk, hogy ez igen bőséges kínálatot jelent. Mindezek az adattípusok
lehetnek változók, konstansok vagy tipizált konstansok típusai.
Élettartamuk
szerint megkülönböztethetünk statikus
és dinamikus változókat. A statikus változó a program futásának minden
pillanatában létezik és elérhető, helyfoglalása az adatszegmensben történik,
mely szegmens maximális mérete 64 kb
lehet. (A konstansok helye a kódszegmens és az adatszegmens között van.) A
dinamikus változók a program futása alatt jönnek létre és szűnnek meg,
helyfoglalása a Heap-ben történik, melynek mérete az adatszegmens többszöröse
is lehet. Dinamikus változók használatánál gondoskodni kell a már nem
használandó változók megszüntetéséről és így az általa lefoglalt memóriaterület
felszabadításáról.
A
változók érvényességi köre szerint
megkülönböztetünk globális és lokális változót. A globális változó a program
bármely részéből látható és használható, értéke minden pillanatban jól
meghatározott, az utolsó rajta végzett műveletek által. A lokális változókat
modulokon belül hozzuk létre és érvényessége is csak az adott modulra terjed
ki, csak abban használható. Másik jellemzője a lokális változóknak, hogy értéke
a modul elhagyásával nem őrződik meg, tartalma véletlenszerű érték lesz, újra
visszatérve a modulba tehát újra inicializálni kell. Ha egy modul által meghatározott
értéket más modulokban, vagy a főprogramban használni szeretnénk, akkor
függvényt kell írnunk. Ez, mint már említettük visszaadott értékkel bír. De azt
is említettük, hogy csak elemi típus visszaadására alkalmas. Ezen a problémán
segít a változó paraméter típus alkalmazása. Ha egy modult változó paraméterrel
hívunk meg, amely természetesen tetszőleges típusú lehet, és ha a modul
gondoskodik arról, hogy a változó paraméter értékét megfelelően beállítsa,
akkor a meghívás után a változó paraméter pozíciójában található változóban a
visszaadott értéket megtaláljuk. Így a visszaadott érték két lépésben
függvényhez hasonlóan elérhető.
Moduláris programozásnál a program feladatának
legjobban megfelelő adatszerkezet megalkotása a legfontosabb programozói feladat.
Egy ügyesen megszerkesztett struktúra már fél siker a helyes program
megírásban. Ezért az adatszerkezet megtervezésénél nagy gonddal kell eljárni.
Főleg akkor, ha nagy adatmennyiséggel dolgozik egy program, mert ha utóbb
kiderül, hogy valamely részadat utólagos elhelyezésére nincs lehetőség, akkor a
teljes adatállományt újra kell szervezni, a rossz adatszerkezetű adatokat be
kell olvasni a háttértárolóról, majd az új szerkezet szerint ki kell írni
azokat. Különben a program az új feladatokra nem lenne alkalmazható.
2.2 Összetett adatszerkezetek
A felhasználó, a
nyelv adta lehetőségeket kihasználva adatait további struktúrákba szervezheti.
A lentebb említett adatszerkezetek mindegyike dinamikusan létrehozott és
valamilyen szisztéma szerint egymáshoz mutatókkal kapcsolódó adatokat
(rekordokat vagy objektumokat) jelentenek. A legfontosabbak adatszerkezetek a
következők:
Multilisták. A multilisták olyan listák, amelyek
elemeinek többféle rákövetője és
többféle megelőző eleme lehet. (Természetesen a mutatók által). Ilyen például a
menürendszer is.
Fák. A fák olyan multilisták,
amelyek nem tartalmaznak kört, azaz nincs a bennük lévő mutatóknak olyan
sorozata, amelynek végigjárásával a kiindulási elemhez visszajutnánk. Ilyen
például a lemez file-szerkezete.
Bináris fák. Olyan fák,
amelynek minden elemében pontosan kettő rákövető található. Rendezésre kiválóan
alkalmas (jobbra nagyobb elemek, balra kisebbek).
Hálók. Olyan multilisták, amelyben körök is
találhatók.
Verem. A verem egy speciális
kezelésű lista. A veremnek általában van megengedett maximális mérete, van
kezdete és van vége. A verembe adatokat tölthetünk, és kivehetünk onnan. Van
egy mutató, amely a verem aktuális üres helyére mutat. Ha a veremmutató a verem
alján (elején) van, akkor üres, adat nem olvasható ki belőle. Ha a mutató a
verem tetején (végén) van, akkor a verem tele van, adatot már nem tölthetünk
bele. A veremből a legutoljára beirt adatot olvashatjuk ki. A verem mérete
általában nem változik. A verem adatait nem szoktuk törölni, hanem betöltéskor
fölülíródnak.
Puffer. A puffer egy
speciális kezelésű zárt lista. Adatok feldolgozás előtti ideiglenes tárolására
alkalmas. A Pufferbe adatokat írunk és veszünk ki belőle. A legelőször bekerült
adatot vehetjük ki legelőször (amely legrégebben vár a feldolgozásra, ezáltal
az adat keletkezésének sorrendjében történik a kiolvasás). A Puffer mérete
általában rögzített. Két mutatóval rendelkezik. Az egyik a kiolvasható elemre
mutat, a másik az első beírható helyre. A mutatók mindig a lista vége felé
mozognak, csak a végéről (a zártság miatt) visszalépnek az elejére. Ha a két
mutató ugyanarra az elemre mutat, akkor a puffer üres – nincs benne adat. Ha
betöltési helyet mutató pointer a kiolvasható elem előtti elemre mutat, akkor a
puffer tele van. A pufferben az adatok nem törlődnek, hanem betöltéskor
fölülíródnak. Minden perifériának van puffere a számítógépben. (Billentyűzet
puffer, videó puffer, lemez puffer, sőt még az operatív memóriának is van: a
Chache memória, vagy gyorsító tár.)
2.3 Adatstruktúrákon végezhető műveletek
(karbantartás)
Az adatstruktúrákon különböző
karbantartási műveleteket kell végezni. Ezek lehetnek a létrehozással,
használattal valamint a megszüntetéssel kapcsolatos tevékenységek. Nézzük meg
ezeket a tevékenységeket egy kicsi részletesebben.
Létrehozás.
A különböző listák és fák dinamikusak méretük tekintetében is, azaz kezdetben
üresek, majd adatokkal töltjük fel őket. Ezeket az adatszerkezeteket először
létre kell hozni. Természetesen a deklarációs részben le kell írni őket, majd
futás közben New eljárással történik a létrehozás. Amikor egy új elemet akarunk
a listára tenni, akkor is a New-t kell használni. Ha a listát már nem
használjuk, akkor Dispose eljárással elemit megszüntetjük, és így az általuk
lefoglalt memóriát felszabadítjuk.
Feltöltés, értékadás. Az adatszerkezethez tartozó
elemek adattároló mezőinek az általunk tárolni kívánt adatokkal való
fölülírása. Kezdetben inicializáló értékekkel, majd a felhasználás során az
aktuális értékekkel. A feltöltés lényegében egy értékadó művelet, mely az
inicializáló értékeket részben vagy teljesen fölülírja.
Beszúrás. Beszúráskor a lista
vagy a fa elemeinek száma növekszik. A beszúrás helye szerint különböző
lehetőségek vannak. Ha a lista nem rendezett, akkor általában a lista végére
szúrunk be. Ha rendezett, akkor a rendezési kulcs szerinti helyre történik a
beszúrás úgy, hogy a kérdéses helyen a láncot szétszakítjuk, és a mutatókat az
új elemre állítjuk, illetve az új elem mutatóit a lista megfelelő elemeire
irányítjuk.
Törlés. Törléskor a lista
vagy a fa elemeinek száma csökken. A törlés helyén a struktúrában maradó elemek
mutatóit egymásra kell irányítani.
Keresés. A keresés célja a
struktúra valamely elemének a megkeresés tulajdonsága vagy az adatszerkezetben
betöltött szerepe alapján. Ennek érdekében a struktúrát, vagy annak egy részét
be kell járni. A keresés eredménye általában a megfelelő elemére mutató
Pointer, vagy az elem megfelelő mezője.
Bejárás. Bejárás alatt
általában teljes bejárást értünk, azaz olyan lépegetés a struktúra elemein,
aminek eredményeképpen szisztematikusan, minden elemet csak egyszer érintve,
végigjárjuk az adatszerkezetet. Ezt általában akkor kell megtennünk, ha
valamilyen eljárást a struktúra minden elemére végre kell hajtanunk, vagy
egyszerűen csak meg szeretnénk számolni az elemeit.
Csere, rendezés. Ha az
adatszerkezet nem rendezett, akkor általában mindegy, hogy egy konkrét elem hol
helyezkedik el a struktúrában. Ha egy rendezetlen adatszerkezetet rendezetté
szeretnénk tenni, akkor a nem megfelelő sorrendben lévő elemeit fel kell
cserélni. Ez többféleképpen lehetséges. Egyik az elem pár teljes fizikai cseréje:
mindkettőt leválasztjuk, és a megfelelő helyre beszúrjuk őket. A másik
lehetőség a tartalom cseréje, azaz a megfelelő mezők értékeit cseréljük ki.
Ebben az esetben a speciális helyzetű (első, utolsó, elágazási ponton lévő)
elemek szerepét újra kell gondolni. Harmadik lehetőség az, hogy a listát egy
kulcstáblán keresztül nézzük, melyekben a mutatók sorrendje a rendezést követi.
Ez a legpraktikusabb megoldás, az adatszerkezetet nem tördeljük szét, fizikai
helye nem változik, ugyanakkor egy listára több rendezett kulcstábla is írható.
Egyetlen hátránya: minden újabb kulcstábla újabb memóriát foglal el. Fizikai
rendezésre sokféle rendezési eljárást kidolgoztak már. Minden konkrét rendezési
feladatnál ki kell választani a megfelelő rendezési eljárást. Ha nagy adathalmazzal
dolgozunk, vagy a rendezést gyakran újra és újra végre kell hajtani, akkor
gyors rendező eljárást illik használni. Ha ez nem áll fent, akkor egyszerűbb
vagy lassúbb eljárást is választhatunk.
3. Objektumok
3.1 Objektumok tulajdonságai
A nem objektumorientált programokban a legnagyobb gond a
modulok és az adatok összehangolt használata. Már a moduláris programozásnál
említettük, hogy a megfelelő adatszerkezet megalkotása nem a legegyszerűbb
feladat. Minél kevesebb globális változó alkalmazására kell törekedni. A
modulokban pedig az ugyanolyan, vagy hasonló funkciókat ellátó változókat
következetesen mindig ugyanúgy deklaráljuk, illetve használjuk. Ennek az
összehangolt változóhasználatnak az objektumok deklarálásában is nagy szerepe
van. Konvenciók segítenek a névválasztásban. A mutatók nevét mindig „P”, az
objektumtípus nevét mindig „T”, a mezőneveket „F”, az inicializáló
paraméterneveket „I” betűvel kezdjük. Az objektumok szerkezete a rekordok
szerkezetéhez hasonlít legjobban.
Az
objektumokban együtt találhatók az adatok (ennyiben hasonlít a rekordra) és a
modulok, amelyeket itt metódusoknak fogunk hívni. Az objektum adatain és
adataival csak saját metódusai dolgozhatnak. Így már olyan egyszerű esetben is,
mint az értékadás illetve értéklekérdezés, metódusokat kell írnunk. Az objektum
mezőinek (adatainak) lekérdezésére GET kezdetű, beállítására (értékadásra) SET
kezdetű metódusneveket használunk. Kezdőértéket beállító eljárás neve: INIT.
Ezen szabályok betartása az objektum sérthetetlenségét, zártságát biztosítják.
Az objektumban az adat mindig megőrződik, az objektum mindig emlékszik saját
állapotára. Az objektumoknak vannak olyan metódusai, amelyek kívülről
elérhetők. Ezek alkotják az objektum interfészét. Az objektumokat ezek
segítségével megszólítjuk, aminek hatására állapotuk megváltozik, esetleg az
objektum további objektumokat aktivizál.
Ha
egy programban több objektumot használunk, akkor azok ugyanazon üzenetre
(megszólításra, metódushívásra) még az ugyanolyan típusú (ugyanazon osztályba
tartozó) objektumok is, lehet, hogy másképpen reagálnak. A reakciójuk ugyanis
függhet az állapotuktól. Ha pedig az objektumhierarchia különböző szintjein
vannak, akkor ugyanolyan néven más-más tartalommal rendelkező metódusai
lehetnek, ezáltal természetes módon nem ugyanúgy reagálnak az eseményekre. Ezt
a tulajdonságot nevezzük sokoldalúságnak, vagy idegen szóval polimorfizmusnak.
Objektum
orientált programjainkban az adatok és modulok összekapcsolása, mint azt
láthatjuk, már megoldódott. De hogyan segíti a kód-újrafelhasználást az
objektum. Nagyon elegáns módon. Ha van egy objektumtípusunk bizonyos mezőkkel
és bizonyos metódusokkal, és ez számunkra még nem elég jó, akkor a meglévő
típusból készítünk egy leszármaztatott típust (ezt az OOP nyelvek természetesen
biztosítják), és az örökölt mezők mellé újakat vehetünk fel, vagy újabb
metódusokat írhatunk, sőt a régi metódusokat fölül is definiálhatjuk. Ezt a
mechanizmust öröklődésnek nevezzük. Az öröklődésnél van még egy fontos
momentum, amely a Unit-okban tárolt modulok kód-újrahasznosításán is túltesz.
Ha egy régi modulunk már nem elég jó, akkor vagy teljesen újat írunk helyette,
vagy kijavítjuk a régi kódlistát. Ha csak használatra kapunk egy modulokat
tartalmazó Unit-ot, akkor erre nem biztos, hogy lehetőségünk van. Ha nem adták
át a kódlistát, akkor azt bizony nem tudjuk megtenni. Objektumok esetén, mint
az a fentiekből is sejthető, nem kell ismerni a kódot, egyszerűen
leszármaztatjuk az újat és az említett módon kijavítjuk. A nem általunk írt
objektumokkal kapcsolatban épp az lehet a legnagyobb probléma, hogy nem tudjuk,
milyen képességekkel rendelkeznek. Ha nincs kódlista, nem tudjuk visszafejteni.
Ezért létfontosságú a pontos dokumentáció. Az általánosan használt
rendszereknek a megismerését jelentősen akadályozza az, hogy nem írnak le
mindent a gyártók (szakmai féltés, jogi dolgok) a teljes körű használathoz,
csak annyit, amennyi feltétlen szükséges. Nekünk kell kutatgatni, megsejteni,
mit – hogyan valósítottak meg az adott rendszerben.
Tehát összefoglalva az objektumok tulajdonságait:
-
adat és kód
együtt található az objektumban,
-
zártság, az objektum adataihoz csak interfészeken keresztül
férünk hozzá,
-
sokoldalúság vagy polimorfizmus,
-
öröklődés.
3.2 Objektumok a Pascal nyelvben
Objektumot a Pascal nyelvben csak főprogramban vagy
Unit-ban deklarálhatunk, modulban nem. A deklaráció típusdeklaráció, ezért a
program Type szakaszába tartozik.
Type
TPont= Object
End;
Az
Object lefoglalt szó, strukturált
párja az End. Az objektum mezőinek
és metódusainak a leírását e két kulcsszó között kell megadni. A metódusoknak
csak a fejét írjuk itt le, kifejtése még a deklarációs szakaszban, azaz a
főprogram Begin-je előtt történik.
Mivel több objektumnak is lehet ugyanolyan nevű metódusa, a kifejtési
szakaszban a metódus nevét minősíteni kell az osztály nevével (típusnevével).
Unitban a kifejtés az Implementációs szakaszban foglal helyet.
Type
TPont= Object
Fx,
Fy: Integer;
Procedure
Init(Ix, Iy: Integer);
End;
Procedure
TPont.Init(Ix, Iy: Integer);
Fx := Ix;
Fy:= Iy;
End;
Mintánkban
egy inicializáló metódust irtunk le, mellyel az objektum mezőinek értéket
adtunk. Természetesen további mezői és metódusai is lehetnek az objektumnak,
illetve e mintában lévő mezők is a felhasználástól függően lehetnek különböző
jelentésűek (síkbeli koordináták, két paramétere valamely algebrai műveletnek,
két szín koordináta stb.) Az objektum típusdeklarációja – a legtöbb nyelvben
osztálydeklarációnak nevezik – csak egy minta az objektum összetételére, szerkezetére.
Objektumot, Pascal terminológia szerint egyedet, Var segítségével vehetünk fel programunkban, a Type szakasz után.
Var Pont1, Pont2: TMinta;
Az
Pont1 illetve Pont2 már két objektum, mely a TPont osztályba tartozik, vagy
másképpen a típusa TPont, és amelyet moduljainkban illetve a főprogramban már
használatba is vehetjük. Például:
Pont1.Init( 50, 100);
POnt2.Init(150, 200);
Ezen
két metódushívás után az Pont1 objektumban Fx értéke 5, Fy értéke 10, az Pont2
objektumban az Fx értéke 15, az Fy értéke pedig 20 lesz mindaddig a program
futása alatt, ameddig azt egy újabb metódushívással meg nem változtatjuk. Azaz
az objektum megőrzi adatait. Természetesen programjainkban, egy adott cél
érdekében, majd jól átgondolt szerkezetű objektumok fogunk deklarálni.
3.3 Statikus és dinamikus objektumok
Keletkezési körülményeik illetve élettartamuk szerint
megkülönböztethetünk statikus és dinamikus metódusokat. Az előző pontban
statikus objektumot deklaráltunk. Nézzük meg, hogyan alakíthatjuk dinamikussá.
Type
PPont= ^TPont;
TPont= Object
Fx,
Fy: Integer;
Procedure
Init(Ix, Iy: Integer);
End;
Ezzel
a deklarációval a PPont révén egy TPont-ra mutató pointerünk lett. Ennek
segítségével dinamikusan deklarálhatunk objektumot programunkban. Most az Pont1
és Pont2 mutató típusú.
Var Pont1, Pont2: PPont;
A programban ezért előbb helyet kell neki foglalni a
New eljárás segítségével.
New(Pont1);
New(Pont2);
Hivatkozni az objektumok metódusaira pedig a
következőképpen kell:
Pont1^.Init( 50, 100);
Pont2^.Init(150, 200);
Ha
a dinamikus objektumunkra már nincs szükségünk, akkor Dispose eljárással
megszüntetjük, felszabadítva helyét a Heap-en.
Dispose(Pont1);
Dispose(Pont2);
Deklarációinkban
a típusnevek első betűi az egyszerű (statikus) deklarációra (T) és a dinamikus
változóra (P) utalnak, mint ahogy azt már fentebb is említettük.
3.4 Öröklődés
Az objektumok legérdekesebb tulajdonságai az öröklődéssel
kapcsolatosak. Öröklődés révén az objektumok hierarchikus rendbe tartoznak. Ha
az ős objektumot egy rajzon legfölül képzeljük el, a leszármazottakat pedig
alatta, akkor a hierarchia egy fához hasonlít. A fa gyökere az az objektum,
amely minden objektumnak közös őse, de ő már nem leszármazottja egyetlen
objektumnak sem. Ez leggyakrabban egy absztrakt osztály, amelyből példányt
sohasem hozunk létre. Minden objektumnak csak egy őse lehet (legalábbis a
Pascal nyelvben), de bármely objektumnak lehet több leszármazottja is. Az
öröklési lánc hosszára semmilyen megkötés nincs, de egyébként is az a helyzet,
hogy egy négy-öt generációs öröklési lánc már olyan bonyolulttá tud válni,
amelyet nehéz átlátni és kezelni. Nagyon ritka tíz vagy annál hosszabb öröklési
lánccal rendelkező hierarchia a mindennapi gyakorlatban. Minden objektum örökli
összes ősének összes adatát és metódusát, és természetesen birtokolja saját
adatait és metódusait is. Nézzük hogyan valósítható meg mindez Pascal nyelven.
Induljunk ki a már fentebb deklarált TPont osztályból, származtassunk belőle
olyat, amely kört valósít meg. A körnek a középpontja lehet az ősének a két
mezője, de a kör megadásához még egy adatra, a sugarára is szükség van.
Type
TKor= Object(Tpont)
Fr:
Integer;
Procedure
Init(Ix, Iy, Ir: Integer);
Procedure
Meretez(Dr: Integer);
End;
Az
öröklés tényét az Object kulcsszó
után zárójelbe tett TPont osztálynév biztosítja. Figyeljük meg a változást. Az
adatoknál a TPont adatmezőit már nem kell felsorolni. Ennek ellenére a TKor
típusú objektumnak van Fx és Fy mezője. Mivel a kör megadásához három adat
kell, az Init metódust újra kellett írni. Az Init kifejtése lehet például a
következő:
Procedure
TKor.Init(Ix, Iy, Ir: Integer);
Inherited Init(Ix, Iy);
Fr:= Ir;
End;
Az
Inherited egy lefoglalt szó,
jelentése: az öröklési láncban az a – ebben az esetben Init nevű - metódus,
amelyet legelőször megtalál a fordító, miközben az öröklési hierarchiában az
aktuális objektumtól a gyökér fele halad. A teljesség kedvéért felvettünk egy
új metódust is, a Meretez-t. Ennek az lesz a funkciója, hogy a kör sugarát megváltoztassa
az átvett értékkel.
Procedure
TKor.Meretez(Dr: Integer);
Fr:= Fr + Dr;
If Fr<0 Then Fr:= 0;
End;
Ennek a metódusnak a meghíváskor a kör sugara Dr-el
megváltozik, amely pozitív Dr esetén növekedést, negatív esetén csökkenést
jelen. A feltétel a negatív körsugár elkerülést szolgálja. Örökléskor tehát az
utódban újabb mezőket deklarálhatunk, fölülírhatjuk az ős metódusait, valamint
új metódusokat írhatunk.
3.5 Késői kötés, virtuális metódusok
Az objektumok viselkedésének a
legszebb, legérdekesebb de egyúttal talán a legnehezebben érthető része
következik most. Először is bővítsük mindkét objektumunkat a következőképpen:
Type
TPont= Object
Fx,
Fy: Integer;
Procedure
Init(Ix, Iy: Integer);
Procedure Show;
Procedure Hide;
Procedure Mozog(Dx, Dy:
Integer);
End;
Type
TKor= Object(Tpont)
Fr:
Integer;
Procedure
Init(Ix, Iy, Ir: Integer);
Procedure
Meretez(Dr: Integer);
Procedure
Show;
End;
Az új metódusok kifejtése:
Procedure
TPont.Show;
PutPixel(Fx, Fy);
End;
Procedure
TPont.Hide;
ClearDevice;
End;
Procedure
TPont.Mozog(Dx, Dy: Integer);
Hide;
Fx:= Fx + Dx;
Fy:= Fy + Dy;
Show;
End;
Procedure
TKor.Show;
Circle(Fx, Fy, Fr);
End;
Nézzük az új metódusok magyarázatát. A Show metódusok
megjelenítik a grafikus képernyőn az objektumokat. A PutPixel eljárás az
aktuális színnel, egy pontot rajzol a képernyő (Fx, Fy) koordinátájú helyére. A
Circle eljárás az aktuális szinnel, (Fx, Fy) középponttal Fr sugarú kört
rajzol. A Hide metódus eltünteti az objektumot, jelenleg a lehető legegyszerűbb
módon, letörli a grafikus képernyőt. Ezek után a Mozog metódus már eléggé
nyilvánvaló: a régi helyéről letörli a pontot, megváltoztatja a helyét, majd
újra kirajzolja. Könnyen belátható, hogy ebben az egyszerű hierarchiában a pont
mozgatása hibátlanul működik.
De nézzük mi a helyzet a körrel, mely metódusok működnek
helyesen, melyek nem. Nyilvánvaló, hogy mindhárom újonnan definiált hibátlanul
működik. De mi a helyzet az örökölt metódusokkal? Mivel a Hide csak egyetlen
képernyőtörlés, ezáltal tökéletesen alkalmas a kör eltüntetésére. De nézzük meg
mi a helyzet a Mozog metódussal, merthogy ezt is érti a TKor, hiszen örökölte.
Mit várunk el egy TKor.Mozog(10, 10) metódushívástól. Azt hiszem eléggé
természetes módon azt, hogy a kör változtassa meg a helyét. Csakhogy ez a fenti
kódok alapján nem fog bekövetkezni. Nézzük meg hogy miért nem. Amikor meghívjuk
a TKor.Mozog(10, 10) metódust, akkor a TPont.Mozog(10, 10) kezd végrehajtódni.
Letörlődik a képernyő, a középpont értékei megváltoznak, eddig még minden
rendben. De ekkor egy Show hívása történik. Vajon mit rajzol a gép. Mivel mit
sem sejt arról, hogy a körnek saját, a ponttól teljesen különböző rajzoló
metódusa van, rajzol egy pontot, és ezzel befejezi a mozgást. Azaz a körünk
ponttá zsugorodott, abszolút helytelenül. Azt kellene elérni, hogy a metódusok
visszataláljanak a hívó objektum saját osztályában található ugyanolyan nevű
(ez esetben Show) metódusához. De vegyük észre, hogy itt egy öröklési folyamat
is zajlik. A „szegény” pont még mit sem sejt arról – mert nem is sejtheti –
hogy őbelőle valaha még kör lesz, és ugyebár a kör nem ugyanaz a látvány, mint
a pont. Amikor a program fordítása zajlik, a fordítóprogram még nem tudhatja –
ha nem tudatjuk vele – hogy a későbbiekben, egy leszármazottja, egyik metódusát
másmilyen tartalommal szeretné használni.
Azokat
a metódusokat, amelyeket eddig megismertünk, statikus metódusoknak fogjuk
nevezni. Azért hívják őket statikusnak, mert a fordítás pillanatában a metódus
ugrási címe a memóriában már ismert és rögzített érték lesz. Fenti példánkban
viszont, ha azt szeretnénk, hogy a kör mozogjon, egy olyan helynek a meghívását
kellene a fordítónak beírni, amit még nem ismer (a TPont fordítása
pillanatában), és amely esetleg majd nem is fog létezik. De a megismerés
lehetőségét bele kellene programozni kódjainkba. Ennek a problémának a
kezelésére alkották meg a metódusok másik csoportját, a virtuális metódusokat.
A fordító a virtuális metódusok fordításakor üres tárhelyeket tart fent egy
táblázatban (Virtuális Metódus Táblában, azaz a VMT-ben), melybe a program
futási ideje alatt kerülnek bele a használható ugrási címek, mely címeket az
objektumok a saját magukra mutató pointerükkel töltenek fel (Self
paraméterrel), mely paramétert látens módon magukkal hordoznak. Így talál
vissza a program a hívó objektum osztályleírásához, és találja meg a megfelelő
virtuális metódust. Szokás ezért ezt a jelenséget futás idejű kapcsolatnak,
vagy késői kötésnek is nevezni. A virtuálissá tételnek a nyelvi megvalósítása
nagyon egyszerű: a deklarációban azokat a metódusokat, amelyeket a leírt módon
szeretnénk használni, virtuálissá kell tenni úgy, hogy a metódusfej után még a
következő lefoglalt szót írjuk: Virtual.
Azaz:
Type
TPont= Object
Fx,
Fy: Integer;
Procedure
Init(Ix, Iy: Integer);
Procedure Show; Virtual;
Procedure Hide;
Procedure Mozog(Dx, Dy:
Integer);
End;
Type
TKor= Object(Tpont)
Fr:
Integer;
Procedure
Init(Ix, Iy, Ir: Integer);
Procedure
Meretez(Dr: Integer);
Procedure
Show; Virtual;
End;
Mint láthatjuk ebben a deklarációban
a Show metódust kellett virtuálissá tenni. Ha egy metódus virtuális, akkor
minden leszármazottjában, minden ugyanolyan nevű metódus is virtuális. Ha nem
tesszük ki a leszármazott deklarációjában a Virtual minősítőt, akkor fordítási hibát kapunk. A kifejtési
szakaszban semmi változtatásra nincs szükség. Felmerülhet ezek után a kérdés,
vajon mikor kell egy metódust virtuálissá tenni? Nem könnyű a kérdésre
válaszolni. Lényegében bármelyik metódus lehet virtuális, csak akkor a
memóriában több helyet foglal el az osztályleírás, mégpedig a VMT-hez szükséges
mérettel. VMT-je ugyanis csak virtuális metódust tartalmazó osztálynak van.
Talán az egyik legjobb válasz az előző kérdésre az, hogy azokat, amelyek
fölüldefiniálását meg szeretnénk engedni, illetve amely metódusokról látszik,
hogy a későbbiekben fölüldefiniálása kívánatos. Absztrakt metódust (amelynek
üres a törzse és csak a származtatás lehetősségét biztosítja) szinte bizonyos,
hogy virtuálissá kell tenni. Biztosan nem kell virtuálissá tenni az Init
metódust, mert egy új osztály e nélkül szinte biztosan nem definiálható,
létrehozáskor ez biztosan meghívódik már az objektum osztályból (nincs a már
részletesen leírt visszadefiniálás), ezért virtuálissá tétele nem indokolt.
Most térjünk át a dinamikus
objektumokra, nézzük meg, milyen újdonságot rejtenek még a létlehozásuk és
megszüntetésükkel kapcsolatos metódusok. Ennek érdekében megismerkedünk további
két metódustípussal, a konstruktorral és a destruktorral. Dinamikus objektumot
eddig csak a New eljárással tudtunk létrehozni. Eláruljuk, hogy a New-nak van
egy úgynevezett kiterjesztett formája, amely egyrészt nem eljárás, hanem
függvény (a visszaadott érték a létrehozott objektum típusra mutató Pointer),
másrészt nem csak egy paramétere lehet, hanem kettő. Kiterjesztett New-t csak
olyan dinamikus objektummal kapcsolatban használhatunk, amelynek van
konstruktora. A konstructor mindig statikus, vagyis nem lehet virtuális, mert
akkor meghívása előtt egy konstruktort kellene meghívni. Általában az Init
eljárást szoktuk konstructorrá minősíteni. Nyelvileg ez úgy történik, hogy az
Init eljárás fejében a Procedure
kulcsszót a Constructor szóra
cseréljük. Azaz a deklaráció:
Type
PPont= ^Tpont;
TPont= Object
Fx,
Fy: Integer;
Constructor
Init(Ix, Iy: Integer);
Procedure Show; Virtual;
Procedure Hide;
Procedure Mozog(Dx, Dy:
Integer);
End;
Ezek után, ha a programban
létre szeretnénk hozni egy PPont típusú dinamikus objektumot, akkor a következő
lehetőségek egyikével élhetünk:
1. New(Pont1);
Pont1^.Init(50, 100);
2. New(Pont1,
Init(50, 100));
3. Pont1:=
New(PPont, Init(50, 100));
Az első megoldás gyakorlatilag
nem ajánlott, hiszen lényegében ugyanazt teszi, mint a második, csak két
lépésben. Ráadásul az első megoldásnál a New eljárásának eredményességéről még
külön meg kellene győződni azért, mert lehet, hogy nem sikerült a helyfoglalás
a memóriában, akkor viszont programhiba következik be. Ha ugyanis ha nincs elég
hely, akkor a Pont1 mutató értéke Nil
lesz, ennek használata viszont legtöbb esetben a gép lefagyását eredményezi.
Erre az esetre a Pascal nyelv a Fail eljárást küldi segítségül, amely szabályos
hibakezelést tesz lehetővé azáltal, hogy kilép a konstruktorból. Ezt az
eljárást alkalmazni viszont csak a 2. és 3. esetben lehet, amikor a konstruktor
(ez esetben az Init) a New második paramétereként szerepel. A második és
harmadik megoldás között aszerint választhatunk, hogy a létlejött dinamikus
objektum mutatójára szükségünk van-e vagy sem. A 2. esetben igen, 3. esetben a
létrejövő mutatót rögtön átadjuk egy másik, pl. listára felfűző eljárásnak.
Figyeljük meg, hogy a 3. esetben a New első paramétere az osztályra mutató
pointer. A kiterjesztett New helyfoglalását gyakorlatilag a konstruktor végzi,
mely természetesen figyelembe veszi a VMT méretét is.
Most nézzük mi a helyzet a dinamikus objektumok
megszüntetésével. Erre való a destruktor, amelynek standard metódusneve a Done.
Mivel ilyen metódus még nincs a TPont leírásában, ezért ezzel most az
osztályleírást kiegészítjük:
Type
PPont= ^Tpont;
TPont= Object
Fx,
Fy: Integer;
Constructor
Init(Ix, Iy: Integer);
Procedure Show; Virtual;
Procedure Hide;
Procedure Mozog(Dx, Dy:
Integer);
Destructor Done;
End;
Remélem
nem meglepő, hogy a Dispose eljárásnak is van egy kiterjesztett formája, amely
kétparaméteres. Csak akkor van értelme destructort írni, ha van az osztálynak
virtuális metódusa, de akkor valószínű, hogy van konstruktora és VMT-je is. Ha
a dispose második paramétere a Done, akkor a Done elvégzi a memória
felszabadítást, melyhez a méretet a VMT-ből olvassa ki.