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.


Verhás Péter Home Page . . . . . . Perl röviden, tartalomjegyzék