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.