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 programozónak gyakran lehet olyan problémája, hogy nagy mennyiségű, gyakran egymással összefüggő, de esetleg különböző elemi típusokhoz tartozó adatokat kell együtt kezelni. Ennek segítésére eleve léteznek a Pascal nyelvben strukturált adatszerkezetek. Ilyenek: tömb, rekord, objektum, halmaz és állomány. A tömbre az jellemző, hogy elemei csak azonos típushoz tartozhatnak. Ha például személyek adatait tömbben szeretnénk tárolni, akkor a következő megoldások lehetségesek. Első esetben annyi és olyan típusú tömböt deklarálnánk, ahány adatát tároljuk a személyeknek. Lenne például egy névtömb (mely stringtömb lenne), lenne egy életkortömb (amely számtömb lenne), lenne egy nemeket (férfi/nő) tartalmazó tömb (amely logikai tömb lehetne), és így tovább. Az azonos indexű elemek a különböző tömbökben egy-egy személy adatait tárolnák. Nem kivitelezhetetlen megoldás, de nem elegáns.

 

Második megoldásként kétdimenziós tömböt használhatnánk. Ez egy táblázatot jelentene, minden eleme string lenne. Az első oszlopban a neveket, a másodikban a stringgé alakított életkort, harmadikban a nemet leíró szót tárolnánk. Felhasználás közben, kiolvasás után, vagy betöltés előtt az adatokat konvertálni kellene, hiszen a Pascal erősen típusos nyelv, a szükséges műveletek (pl. összegzés) csak a neki megfelelő típusban hajható végre.

 

Ennél a megoldásnál is van jobb. A jobb megoldás a rekord. A rekordnak különböző típusú és annyi darabszámú mezője lehet, amennyire csak szükség van. A rekordokban való adattárolás már elegáns, s majdnem a legjobb megoldás is egyúttal. Sőt, a Pascal a típusos (lemezes) állomány használatával segít az adatok könnyű tárolhatóságában is. A típusos állomány egy-egy adata ugyanis lehet egy teljes rekord, amelyet egyetlen művelettel kiírhatunk lemezre, vagy beolvashatjuk be onnan – egy megfelelő rekordba. A halmaz csak ritkán használt összetett típus. Leggyakrabban valamely változó lehetséges értékeit szoktuk halmazban tárolni.

 

A legjobb tárolást az adataink számára – remélem nem meglepő módon - az objektumok biztosítják, mert az adatok mellett még az adatokon operáló metódusokat (eljárásokat és függvényeket) is egy zárt egységben tárolhatjuk segítségével. Hogy hogyan, azt majd a későbbiekben részletezzük.

 

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:

 

            Listák. A listák elemei olyan azonos szerkezetű adatok, amely adatnak egy része egy mutató, amely a lista következő elemére mutat. Ez az egyirányú láncolt lista. Ha minden elem tartalmazza az előtte lévőre mutató mutatót is, akkor az, kétirányú láncolt lista. A lista lehet rendezett is, ha az valamely mezője szerint rendezve tartalmazza az elemeket. Lehet a lista nyitott, vagy zárt. Zárt, ha az utolsóelem rákövetője az első elem.

 

            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);

Begin

  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);

Begin

  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);

Begin

  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;

Begin

  PutPixel(Fx, Fy);

End;

 

Procedure TPont.Hide;

Begin

  ClearDevice;

End;

 

Procedure TPont.Mozog(Dx, Dy: Integer);

Begin

  Hide;

  Fx:= Fx + Dx;

  Fy:= Fy + Dy;

  Show;

End;

 

Procedure TKor.Show;

Begin

  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.