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...
Sziasztok!
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( )
{
chop;
my ($name, $number) = split /\t/;
$self->AddEntry($name, $number);
}
close BOOK;
return "";
}
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:
$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 = ;
chop($name);
Emlékeztetõül: a mindig egy teljes sort olvas be a
fájlból. (Másképp fogalmazva: addig olvas, amíg \n-t nem
talál) A chop()levágja az argumentumként megadott string utolsó
karakterét. (Itt arra használjunk, hogy megszabaduljunk a $name változó
végéhez ragadt \n-tõl, ami akkor került oda, amikor a
begépeléskor leütöttük az ENTER-t.)
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.
|