Ez a fejezet egy kicsit kilóg a sorból, ezt ugyanis nem én írtam, hanem Mörk Péter (t-pmork@microsoft.com). Ez a fejezet hiányzott, Péter pedig írt nekem egy levelet, hogy ő ezt egyszer már megírta. És szerintem jó. Akkor meg minek kétszer feltalálni a melegvizet...
Ez a levél úgy született, hogy valamelyik vasárnap délután Dublin belvárosában sétálva egyszer csak megvilágosodott előttem az objektum -orientált programozás lényege. Ez pedig nem más, mint két angol szóba tömörítve: "code reuse". A most következő bevezetőben tehát lerántom a fátylat az objektum- orientált programozásról :-), utána bemutatok egy saját gyártmányű működöképes (ténlyeg!) objektum-orientált példaprogramot Perlben megírva, részletesen kitérve a Perl szokás szerint "patologically eclectic" megoldásaira.
Az objektum-orientált programozás klasszikus példája a következő: Vegyünk egy általános síkidom-osztályt, aminek van egy draw() függvénye. Származtassunk ebből egy "téglalap", egy "háromszög" és egy "kör" osztályt; a leszármazott osztályokban mindegyikben lesz egy-egy draw() függvény, tehát ugyanazt a függvényt fogjuk használni a téglalap, a kör, illetve a háromszög ábrázolásához. Hurrá!
A példa azért tipikus "marketing bullshit", mert azt sugallja, hogy a draw() függvényt elég egyszer megírnunk, a téglalap, a háromszög és a kör ezt örökli, vagyis kódírást takarítottunk meg.
Sajnos nem ez a helyzet. Objektum-orientáltan is ugyanannyi kódot kell írnunk, mint anélkül, mint ahogyan struktúráltan programozva is ugyanannyi munkánk van, mintha mezítlábas tömbökkel dolgoznánk. Az előny nem abból ered, hogy valamit meg tudunk spórolni, hanem abból, hogy objektum-orientáltan programozva a kód struktúráltabb, magyarán ÁTTEKINTHETŐBB lesz.
Nagyon jó példa erre a Windows rendszerhívások gyűjteménye (Application Programming Interface, röviden API; ahogy a Microsoft terminológia nevezi.) Ezt még akkor kezdték el fejleszteni, amikor Bill Gates azt nyilatkozta a Borland Turbo Pascalról, hogy "Ha az objektum-orientált programozás tényleg olyan nagy durranás, akkor mégis miért van az, hogy az alkalmazásokat jórészt sima C-ben írják?" Már a Windows 3.1 API is nyolcszáz körüli függvényből állt, amit ajánlatos volt fejben tartania a programozónak, hacsak nem akarta programozás közben folyton a könyvet bújni, a Win32 API pedig teli van csupa hasonló nevű, hasonló funkciójú, de kissé eltérő paraméterezésű függvényekkel (CreateDialog, CreateEvent, CreateMailslot, stb. Közel hetven olyan függvény van, aminek a nevében szerepel a "Create" szó...)
Pedig mennyivel egyszerűbb lenne a dolog, ha valamilyen szisztéma szerint csoportosítanánk a függvényeket. Nos, az objektum-orientált programozás éppen ezt teszi. (A Microsoft Visual C++ osztályokat definiál a Win32 API függvények csoportosítására. Osztályokból is nagyon sok van, ez tehát önmagában még nem teszi triviálissá a programozást, de némileg egyszerűsíti a dolgot.)
A kettes számú használható ötlet az, hogy az így képezett függvény-csoportokat hozzárendeljük egy adatstruktúrához. Ha előrelátóak vagyunk, akkor épp ahhoz, amelyiken műveleteket végeznek :-) Ezt az adatstruktúrát, a hozzá rendelt függvényekkel együtt objektumnak hívjuk, innen a módszer elnevezése.
Az objektumok adatokat tárolnak, amelyeket az objektum függvényeivel lehet manipulálni. Ha igazán civilizáltan akarunk programozni, akkor az objektum adatait csak függvényein keresztül olvasssuk és módosítjuk. Ezzel megvalósul az "adat-enkapszuláció" (Császár Péter szép szava), azaz az objektum belső szerkezete rejtve marad a programozó elől, így egyrészt nem szükséges megtanulnia, hogy az hogyan működik belülről, másrészt elkerülhető, hogy az objektum belsejébe nyúlva véletlenül elbarmoljon valamit.
Az objektumokat az osztályokból hozzuk létre, tehát az osztályok a "minták", amik alapján az objektumok készülnek. A harmadik ötlet az, hogy az oszályok egymásból származtathatók: ilyenkor a leszármazott osztály örökli a szülő függvényeit. Csakhogy: ez még önmagában nem jelenti azt, hogy a származtatott osztályban (és az ebből létrehozott objektumokban) minden további nélkül használhatjuk az öröklött függvényeket. A klasszikus példában például nem ez a helyzet, mivel egy kört nyilvánvalóan másképp kell megjeleníteni, mint egy téglalapot. A draw() függvényt tehát újra meg kell írnunk, a származtatott osztály igényeinek megfelelően. Mi ebben a buli? Egyrészt az, hogy nem biztos, hogy minden függvényt újra kell írnunk. A másik, hogy miután megírtuk a szükséges új függvényeket, a háromszöget ábrázoló függvényt ugyanúgy draw()-nak hívják majd, mint a kört vagy a téglalapot ábrázoló függvényt, ha tehát valaki más akarja használni az objektumainkat, aki nem ismeri pontosan a függvények belső felépítését (ez a valaki mi magunk is lehetünk, pár évvel később), az nem három különféle rajzolófüggvényt lát, hanem csak egyet (és mellé három különféle osztályt). Code reuse rulez.
Az öröklésnek van egy fájdalmas velejárója is: ha saját magunk hozunk létre osztályokat a korábban már meglévőkből, az objektumok megszűnnek fekete doboznak lenni. Amint módosítani akarunk valamit egy osztályon, rögtön szükségünk van az osztályok belső felépítésének pontos ismeretére, másképp nem tudnánk megírni a szükséges új függvényeket. A "fekete doboz"-ként kezelhetőség tehát csak az OBJEKTUMOK használóira vonatkozik, az osztályok újrafelhasználóira nem! Ez az örökösödési adó, amit a szülőosztály függvényeinek örökléséért kell fizetnünk.
Perlben is lehet objektum orientáltan programozni, mindjárt el is mesélem, hogy hogyan:
ELSŐ RÉSZ: a hozzávalók
Mint tudjátok, a Perl-ben minden változó globális, kivéve amit lokálisnak
definiálunk. Ez nem mindig kényelmes, ezért bevezették a package fogalmát: a
package kulcsszóval el lehet a program részeit elválasztani egymástól. Ha ezt
írjuk:
package Egyik;
$global = "egyik";
.
.
.
package Masik;
$global = "masik";
.
.
.
.
Akkor az Egyik package-ban lévő $global változó globális lesz az Egyik package
függvényeire nézve, miközben a Masik package függvényei erről mit sem tudnak.
Nekik a $global értéke "masik". Célszerűen úgy szokták szervezni a dolgot,
hogy a package-k külön fájlokba kerülnek, és az use operátor segítségével
emelik be őket a program elején.
Beemelni bármilyen perl programot lehet egy másikba, ez azzal egyenértékű, mintha a két programot futtatás előtt egyetlen fájlba másoltuk volna össze. A gyakran használt függvényeket ki szokták tenni egy külön fájlba és utána beemelik a scriptbe, ha használni szeretnék a függvénykönyvtár valamelyik függvényét. A nagyon gyakran használt függvénykönyvtárakat a perl\lib könyvtárba teszik és a ".pm" kiterjesztést adják neki (pm annyit tesz: Perl Module). A Perl modult tartalmazó fájl neve ugyanaz, mint a modul neve.
Van még egy kis kavarás az "use" és a "require" közötti árnyalatnyi különbséggel, de ennek most a történetünk szempontjából nincs szerepe, ezért inkább hallgatok róla.
Ahhoz, hogy objektum orientált programot írjunk, lényegében három dologra van szükség: objektumokra, osztályokra és metódusokra.
objektumok
Az objektumokat a Perlben referenciák (mutatók) testesítik meg. Természetesen
kell valami megoldás arra, hogy az objektumra mutató referenciákat
megkülönböztessük a közönséges referenciáktól. Ezt úgy tesszük meg, hogy az
objektumok referenciáit "megszenteljük" a bless utasítás segítségével. A
blessed referencia mindössze annyiban különbözik a közönséges referenciától,
hogy a Perl tudja róla, hogy ez egy objektumot jelent és azt is, hogy ez az
objektum melyik osztályba tartozik. A $mokus referenciát a következő módon
tehetjük a Erdolakok osztály objektumává:
bless $mokus, "Erdolakok";
A közönséges referenciával csak a változóra hivatkozhatunk, amelyikre a
referencia hivatkozik:
${$ref} = "bikmakk";
Az objektum-referenciával egyrészt hivatkozhatunk az objektum adataira:
${$objref} = "object-bikmakk";
másrészt meghívhatjuk az objektum függvényeit:
$objref->ThisIsAMethod();
Ezeket a függvényeket mostantól metódusoknak hívjuk.
Természetesen egy objektum-referencia csak egyetlen változóra mutathat. A gyakorlatban nem sokra mennénk egy olyan objektummal, aminek egyetlen adata egy mezítlábas skalár; szerencsére a Perlben vannak hash listák is, és persze az objektum-referencia mutathat hash-listára is, arról pedig már tudjuk, hogy gyakorlatilag bármiből bármennyit tartalmazhat.
A gyakorlatban tehát úgy hozunk létre objektumot, hogy a bless utasítással objektum-referenciaként deklarálunk egy üres hash-listára mutató referenciát. A hash listán aztán az objektum valamennyi saját adata tárolható.
osztályok
Az osztály nem más, mint egy package, a package-ban definiált függvények pedig
az osztály metódusai. Amikor a bless-el létrehozunk egy objektumot,
megadhatjuk, hogy az objektum melyik osztályba tartozzon. (Ha nem adunk meg
típust, akkor a létrejött objektum abba az osztályba fog tartozni, amelyik
package-ban a bless utasítást kiadtuk.)
metódusok
Az osztály-package függvényei az osztály metódusai. Amikor létrehozunk az
osztályba tartozó objektumot, az új objektum megkapja az osztály metódusait.
Ezeket mostantól objektum-metódusoknak hívjuk. Az osztály metódusait az
osztálynéven keresztül hívjuk meg:
$mokus = Erdolakok->create();
Az objektum metódusait pedig az objektumra mutató referencián keresztül:
$mokus->EatNuts("chesnut");
$mokus->EatNuts("walnut");
Van egy lényeges különbség az osztály-medódusok és az objektum-metódusok
között. Amikor egy osztály-metódust hívunk meg, a Perl az átadott
argumentumlista elé automatikusan odailleszti az osztály típusát. A
leggyakrabban meghívott osztály-metódus a konstruktor függvény: ezt osztály-
metódusként hívjuk meg, és egy objektum-referenciát ad vissza. (A konstruktor
függvény neve bármi lehet, csak az a lényeg, hogy egy objektum-referenciát
hozzon létre.)
Ezzel szemben az objektum-metódus meghívásakor nem az osztály típusa, hanem az objektum mutatója kerül a paraméterlista elejére. Erre az objektum-metódusnak mindenképp szüksége van, másképp nem tudna hozzáférni az objektum saját adataihoz.
Az osztály-medótusok és objektum-metódusok deklarációja között nincs formai különbség. Sőt, mind a két féle képpen meghívhatjuk őket. Ha egy metódus objektumon végez műveletet, akkor természetesen nem hívhatjuk meg osztály- metódusként, mert hibaüzenetet kapunk. A metódusok megírásakor ezt figyelembe kell vennünk. A gyakorlatban ez nem olyan nagy probléma: minden függvényt objektum-metódusként használunk, kivéve a konstruktort, amit osztály- metódusként hívunk meg. Ha nagyon bolondbiztos kódot akarunk írni, a paraméterlista első eleméből eldönthetjük, hogy a függvényt osztály- metódusként, vagy objektum-metódusként hívták-e meg. Van értelme annak, hogy egy metódust egyszer így, másszor meg úgy hívjuk meg: ezért nincsenek kitiltva a nyelvből. (Hogy mi az értelme, arról talán majd legközelebb.)
Nézzünk egy egyszerű osztály-metódust:
package Erdolakok;
sub create
{
my $type = shift;
my $self = {};
return bless $self, $type;
}
Ha osztály-metódusként meghívjuk ezt a függvényt, akkor a következő történik:
a Perl ugyebár a paraméterlista elé beszúrja az osztály nevét. Ezt mindjárt ki
is vesszük a $type változóba a shift-tel. A második sorban létrehozunk egy
hash listára mutató üres referenciát, a harmadik sorban ebből objektumot
csinálunk és visszaadjuk a hívónak. Ha azt mondjuk, hogy:
$mokus = Erdolakok->create();
akkor létrehozunk egy, az Erdolakok osztályba tartozó objektumot. Ahhoz, hogy
a mókus diót és mogyorót is tudjon enni, megfelelő metódusra is szüksége van.
Például valami ilyesmire:
sub EatNuts()
{
my $self = shift;
my $food = shift;
if($food eq "chesnut")
{
# ide jön a dióevés implementációja
}
elsif($food eq "walnut")
{
# ide jön a mogyoróevés implementációja
}
else
{
print "I can't eat this $food\n";
}
}
Ha objektum-metódusként hívjuk meg ezt a függvényt, akkor a Perl az objektum
referenciát teszi a paraméterlista elejére, amit mindjárt át is veszünk egy
lokális változóba a függvény elején. Ez a lokális változó használható azután
az objektum belső adatainak eléréséhez. Valahogy így:
if($food eq "chesnut")
{
$self{'FOOD_CONSUMED'} = $self->EatChesNut();
$self{'HUNGRY'} = "no";
$self{'WATER_CONSUMED'} = $self->DrinkWater();
$self{'THIRSTY'} = "no";
$self{'HAPPY'} = "yes";
}
rövid összefoglalás
A Perl-ben az osztályok package-k, amelyek függvényeket tartalmaznak. Ezeket a
függvényeket a Perl (és a Pascal is) metódusoknak nevezi. Az osztály
metódusait az osztály nevén keresztül hívhatjuk meg, bár ez - az objektumokat
létrehozó osztály-metódus kivételével - ritkán szokás.
Az objektum nem más, mint egy változóra (általában hash listára) mutató referencia, amit a bless operátorral hozzárendelünk egy objektum osztályhoz. Amikor létrehozunk egy objektumot, az automatikusan megkapja az osztály metódusait. Ezáltal az osztályban definiált metódusok az objektum objektum- metódusaivá válnak.
Az objektum-mutatón keresztül meghívhatjuk az objektum metódusait, illetve hozzáférhetünk közvetlenül az objektum adataihoz. A Perl nem rejti el az objektum változóit a programok elől, viszont elvárja a programozótól, hogy ne turkáljon az objektumok saját adataiban. ("A Perl module would prefer that you stayed out of its living room because you were not invited, not because it has a shotgun. /Larry Wall /")
Ennyi bevezetés után következzék egy (működőképes!) példa:
MÁSODIK RÉSZ: hab a tortán
A történet a következő:
Van egy telefonszámokat tároló szövegfájlunk, ami kb. így néz ki:
Peter 6797
Peter 2897618
Peter +3646381385
David 1234
Miki 3456
Arpad 4567
Miki 3456
Zoli 3332
Ali 4562
Fontos: A nevet tabulátor karakter (\t) választja el a telefonszámtól.
Egy olyan objektumot szeretnénk készíteni, ami elrejti előlünk a szövegfájlt és metódusokat ad a telefonkönyv kezelésére. Első lépésként csak annyit akarunk elérni, hogy létre tudjunk hozni egy ilyen objektumot (ez eléggé lényeges) és hogy név szerint tudjunk keresni az objektum által reprezentált adatbázisban.
A telefonkönyv-objekum létrehozásakor megnyitjuk a fájlt és a tartalmát beolvasssuk egy hash-listába. A keresést ezen a listán végezzük, hogy ne kelljen újra beolvasni a szövegfájlt minden egyes alkalommal, amikor meg karunk keresni egy számot.
A név-telefonszám párokat hash listán tároljuk, a nevet használva kulcsként.
Egy kis komplikáció: a hash-listák kulcsai egyediek. Ez azt jelenti, hogy ha
egy emberhez több telefonszám is tartozik, azt csak úgy tudjuk tárolni, hogy
nem skalárokat, hanem tömböket tárolunk a hash listán. Valahogy így (az egymás
alatti pontok a telefonszámokat tartalmazó tömbök elemeit jelképezik):
Peter - David - Miki - Arpad - Zoli - Ali
. . . . . .
. . . . . .
. . .
. .
.
Az osztályt Perl modulként írtam meg: ez azt jelenti, hogy a perl/lib
könyvtárba kell tenni, és a fájl nevének meg kell egyeznie a modul nevével. A
package-okat nem kötelező modulként megírni, az egész példaprogramot rakhattam
volna egyetlen fájlba is. Azért válaszottam mégis szét, hogy jobban
elkülönítsem a metódusokat definiáló objektumosztályt az objektumot használó
kódtól.
Kell először is egy konstruktor metódus:
sub New
{
my $type = shift;
my $self = {};
bless $self, $type;
my $status = $self->Init( @_ );
print $status, return "" if $status;
return $self;
}
Figyeljük meg, hogy a bless művelet (az objektum létrehozása) után máris
használhatjuk az objektum metódusait: itt például az Init() metódust hívjuk
meg. A visszatérési érték a hibaüzenetet tartalmazza. Ha valami gixer volt,
akkor kinyomtatjuk a hibaüzenetet és üres stringet adunk vissza a hívónak, aki
innen tudja meg, hogy az objektumot nem sikerült létrehozni. Ha a hibaüzenet
üres string, akkor az objektum-referenciát visszaadjuk a hívónak és ezzel az
objektum megkezdi szoftver-életét.
Adós maradtam az Init() függvénnyel, pedig a konstruktornak szüksége van rá.
Az Init() ugyan objektum-metódus, de csak a konstruktor osztály-metódus
használja.
sub Init()
{
my ($self, $file) = @_;
open(BOOK, $file) or return "Init() failed: can not open $file\n";
while(
Erről megint nem tudok többet mondani, mint amit már elmondtam a korábbi
leckékben. Az egész osztály legérdekesebb függvénye az AddEntry():
sub AddEntry()
{
my ($self, $name, $number) = @_;
if($self{$name})
{
push @{$self{$name}}, $number;
}
else
{
$self{$name} = [ $number ];
}
}
Vadul néz ki, pedig nagyon egyszerű dolgot csinálunk. Először is megnézzük,
hogy a megadott név szerepel-e már a listán:
if($self{$name})
ha nem, akkor hozzáadunk a listához egy újabb elemet. Ennek az elemnek a
kulcsa a $name változóban tárolt string, értéke pedig egy tömb (erre utalnak a
[] jelek), aminek egyelőre egyetlen eleme a $number változóban tárolt
telefonszám.
$self{$name} = [ $number ];
Egy árnyalattal bonyolultabb a helyzet, ha a név már létezik: ilyenkor a már
létező tömbhöz kell adunk egy újabb elemet. Szerencsére a push utasítást pont
erre találták ki:
push @{$self{$name}}, $number;
(Kicsit sok benne a kukac meg a dollár, de ha nézitek egy ideig akkor
rájöttök, hogy mindenből pont annyi van, amennyi kell. Ha nem hiszitek, akkor
futtasátok le a programot - működik :-)
Ezek után már csak a kereső metódusra van szükség. Íme:
sub LookupByName
{
my ($self, $name) = @_;
return $self{$name};
}
(Ennél egyszerűbb metódust nagyon nehéz nenne írni :-)
HARMADIK RÉSZ: csokoládé díszítés
lássuk most ezek után a főprogramot, ami az előbb definiált osztályból
létrehozott objektumot használja! Mindjárt az elején meg kell mondanunk, hogy
melyik osztályt szeretnénk használni:
use Phone;
Ha ez megvan, akkor meghívjuk az _osztály_ kreátor függvényét, hogy
létrehozzuk vele a $konyv nevű objektumot. A telefonszámokat tartalmazó fájl
elérési útját paraméterként adjuk meg.
A program során később is bármikor meghívhatjuk az osztály bármelyik
függvényét, ebben a példában viszont csak a már létező objektum metódusaival
operálunk. Ennyi szöveg után egy kis szintaktika: a Phone osztály New
metódusát a következő módon hívjuk meg:
$konyv = Phone->New("e:/home/stsmork/script/book.txt");
Ha a $konyv változóban tárolt visszatérési érték nulla, akkor az objektumot
nem sikerült létrehozni. Minden más esetben egy hash listára mutató
referenciát kapunk. Ez a "blessed" hash lista tárolja az objektum adatait. A
hash referencia "megszentelt", ezért a Perl tudja róla, hogy egy objektum-
osztályhoz tartozik és azt is, hogy melyikhez.
A $konyv objektum metódusait szintén a -> operátor segítségével hívhatjuk meg.
Bill Gates telefonszámát például így kérdezhetjük le:
A teljes példaprogram
phone.pm ...
telefon.pl ...
book.txt
ebben a három fájlban található meg.
Külön köszönet Császár Péternek, az objektum-orientált programozás elméleti
kérdéseivel kapcsolatos konzultációért.
$Aref = $konyv->LookupByName( "Bill Gates" );
A visszatérési érték a telefonszámok tömbjére mutató referencia. Ennek a
referenciának a segítségével már gyerekjáték kinyomtatni a megtalált
telefonszámokat:
foreach $number ( @{$Aref} )
{
print "$number\n";
}
A teljes program a fenti műveleteken kívül még három másik dolgot csinál:
ellenőrzi, hogy az objektumot sikerült-e létrehozni, ellenőrzi, hogy a
LookupByName metódus adott-e vissza értéket, valamint a keresés előtt
beolvassa a konzolról a keresendő nevet a $name változóba:
print "\nName: ";
$name =
Emlékeztetőül: a