Weblabor - A kiindulopont webmestereknek
Leírások+Referenciák / Perl röviden / Objektum-orientált programozás

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.