Tvorba objektů
Doposud jsme slovo „objekt“ používali pro označení jakékoliv „věci“, se kterou bylo možno manipulovat pomocí proměnné nebo odkazu (skaláry, pole, hashe, podprogramy, I/O objekty, regexpy, typegloby a možná další). V kontextu objektového programování budeme objektem (až na označené výjimky) nazývat pouze instanci nějaké třídy, tedy takový objekt (věc), který náleží nějaké třídě. Perl na to nahlíží podobně: objektovým objektem je cokoliv, co má u sebe označení, k jaké třídě to patří (a nejen Perl, ale většina programovacích jazyků, nezávisle na tom, jakou syntaxi prezentují programátorům). Přiřazení třídy objektům se provádí pomocí vestavěné funkce bless
. V Perlu se manipulace s objekty provádí téměř výhradně pomocí referencí a operátoru ->
, proto i argumenty funkce bless
jsou reference na objekt a řetězec reprezentující třídu. Příslušnost referencovaného objektu k třídě lze zjistit operátorem ref
.
my $ref = {}; print ref $ref; # HASH # Hash, na který ukazuje $ref, bude nyní ve třídě Trida bless $ref, "Trida"; print ref $ref; # Trida
Je potřeba mít na paměti, že je to ta věc, nikoliv reference na ni, která má příslušnost k třídě.
my $ref1 = {}; my $ref2 = $ref1; bless $ref2, "Trida"; # Reference ukazují na tentýž objekt, který byl přidán do třídy Trida, # čili následující vypíše také Trida print ref $ref1;
V obskurním případě můžeme přiřadit do třídy i objekt, který máme přímo v proměnné.
my %hash; bless \%hash, "Trida"; my $ref = \%hash; print ref $ref; # Trida
„Požehnat“ můžeme téměř jakémukoli objektu (věci). Klasické objekty používají hojně hashe (některé možná pole), inside-out objekty (viz příště) používají skaláry. Omezení se ale nekladou.
my $r1 = qr/^ahoj/; my $r2 = sub { print 'ahoj'; }; bless $r1, "Trida"; bless $r2, "Trida";
Speciálním případem jsou reference na I/O objekty, které jsou automaticky členy třídy IO::Handle
nebo některé její podtřídy. (Alespoň pokud nainstalovaný Perl není vykopávka z doby ledové, nebyl přeložen bez toho, či netrpí poruchou osobnosti.)
open my $handle, ">out.txt"; use IO::Handle; $handle->print('ahoj');
Volání metod
Poslední příklad ukázal nejběžnější způsob volání metod objektu, tj. pomocí šipkového operátoru ->
. Pokud je na levé straně šipky reference na blessovaný objekt, překládá se volání
objekt->metoda(parametry)
jako Trida::metoda(objekt, parametry)
, kde Trida
je třída, do které náleží objekt
, tj. zavolá se podprogram metoda
z modulu Trida
a jako první parametr dostane naši referenci na objekt.
Pokud je na levé straně šipky jméno třídy, překládá se volání Trida->metoda(parametry)
jako Trida::metoda(q/Trida/, parametry)
, tj. jako první parametr se předá jméno třídy. Takovéto volání se pak chová jako volání metody pro celou třídu (namísto pro jeden objekt). Jednu metodu je možno zavolat oběma způsoby. V těle metody můžeme zjistit způsob volání pomocí otestování prvního parametru operátorem ref
.
Konstruktor objektů bývá obvykle implementován jako třídní metoda, která vrátí referenci na objekt, případně jej inicializuje.
package Pes; sub new { return bless {}, shift; } sub zvuk { return "Haf"; } package Kocka; sub new { return bless {}, shift; } sub zvuk { return "Mnau"; } package main; my @zvirata = ( Pes->new(), Kocka->new() ); for my $zvire (@zvirata) { print $zvire->zvuk(), qq/\n/; }
V příkladu jsme zadefinovali dvě třídy – Pes
a Kocka
. Objekty těchto tříd jsou hashe, prozatím prázdné – objekty nemají žádné atributy. Obě třídy umí vytvořit objekt a vrátit zvuk charakteristický pro zvíře. Poznamenejme, že momentálně lze získat zvuk voláním metody zvuk
přes objekt i přes třídu.
Dejme tomu, že bychom chtěli, aby každé zvíře mělo atribut – jméno. Toto jméno a zvuk zvířete pak můžeme použít v metodě projev
.
package Pes; sub new { my $self = bless {}, shift; $self->{jmeno} = shift; return $self; } sub zvuk { return "Haf"; } sub projev { my $self = shift; my $str = ref $self ? $self->{jmeno} . " dela" : "Psi delaji"; return $str . q/ / . $self->zvuk(); } package Kocka; # obdobně … package main; my @zvirata = ( Pes->new("Jonatan"), Kocka->new("Micka") ); for my $zvire (@zvirata) { print $zvire->projev(), qq/\n/; } print Pes->projev(), qq/\n/;
V metodě projev
nyní pomocí operátoru ref
rozlišujeme, zda jsme voláni přes objekt nebo třídu. Výstup příkladu bude:
Jonatan dela Haf Micka dela Mnau Psi delaji Haf
Konstruktor new
můžeme také volat jako metodu již existujícího objektu. Současný kód by se s tím nevyrovnal, ale můžeme jej upravit například takto:
package Pes; sub new { my $trida = shift; $trida = ref $trida if ref $trida; my $self = bless {}, $trida; $self->{jmeno} = shift; return $self; } # a pak my $jonatan = Pes->new("Jonatan"); my $komisar = $jonatan->new("Jonatan"); # nebo také my $komisar = Pes->new("Jonatan")->new("Rex");
Některé moduly, zejména Tk
používají tuto vlastnost k zřetězené výrobě hierarchie objektů. Jiné zase používají volání konstruktoru přes objekt jako kopírovací konstruktor, tj. klonování objektu. Tak nebo tak, takovéto použití může do vašeho kódu přinést jistou dvojznačnost a nepřehlednost. Rozhodnutí je vždy na tvůrci daného modulu.
Nepřímé volání metod
Perl také podporuje druhou cestu, jak volat metody. Místo neco->metoda(parametry)
(kde neco
je třída nebo objekt) můžeme napsat metoda neco parametry
. Mezi neco
a parametry
v tomto případě není čárka. (Pokud jste se až do teď divili, proč ve volání print STDERR 'ahoj'
není čárka, tak vězte, že jde o přesně tento způsob volání.)
Například namísto Pes->new("Jonatan")
bychom mohli napsat new Pes "Jonatan"
. Toto vypadá skoro stejně jako volání konstruktoru v jazycích podobných C++. Bohužel pouze vypadá, a proto se nedoporučuje tento způsob volání používat. Největší škodu může napáchat při volání metod bez parametrů (např. zvuk $jonatan
). Pokud bychom měli kdesi funkci zvuk()
, zavolala by se tato jako zvuk($jonatan)
namísto Pes::zvuk($jonatan)
. U metod s parametry toto konkrétní nebezpečí nehrozí, ale se šipkovou konvencí nemusíme na takové věci vůbec myslet, a proto se jí budeme držet.
Dědičnost
Významnou vlastností OOP je dědičnost (a dle mého názoru jedna z nejvíce motivujících pro použití OOP). V našich třídách Pes
a Kocka
je hodně společného kódu, který je možno sjednotit do nadřazené třídy Zvire
. Nadřazenou třídu specifikujeme v Perlu pomocí speciální proměnné @ISA
(„is a“). V tomto poli uvádíme jednoho nebo více předků současné třídy. Pokud naše třída nějakou metodu neimplementuje, Perl začne tuto metodu prohledávat (rekurzí směrem do hloubky) v nadřazených třídách. Pokud neuspěje, poslední šancí je předdefinovaná třída UNIVERSAL
. (Ještě se do toho plete něco, co se jmenuje autoloader, ale o tom někdy jindy.)
package Zvire; use Carp; sub new { my $self = bless {}, shift; $self->{jmeno} = shift; return $self; } sub zvuk { croak "Toto zvire neumi zadny zvuk!"; } sub projev { my $self = shift; my $str = ref $self ? $self->{jmeno} . ' dela' : 'Zvirata typu ' . $self . ' delaji'; return $str . q/ / . $self->zvuk(); } package Pes; our @ISA = qw/Zvire/; sub zvuk { return "Haf"; } package Kocka; our @ISA = qw/Zvire/; sub zvuk { return "Mnau"; } package main; my @zvirata = ( Pes->new("Jonatan"), Kocka->new("Micka") ); for my $zvire (@zvirata) { print $zvire->projev(), qq/\n/; } print Pes->projev(), qq/\n/;
Použitím dědičnosti jsme podstatně zkrátili implementaci jednotlivých zvířecích tříd, které nyní obsahují pouze své specifické vlastnosti (metodu zvuk
). Jak konstruktor, tak metoda projev
se dědí přímo z nadřazené třídy Zvire
. Všimněme si, že při konstrukci objektu třídy Pes
je zděděný konstruktor volán jako Zvire->new('Pes', …)
a příslušný objekt je tedy správně zařazen do třídy Pes
. Toto funguje, protože jsme použili variantu funkce bless
se dvěma parametry. Funkce bless
s jedním parametrem vždy přiřazuje objekt do třídy, kde je volána, tudíž bychom objekt degradovali do třídy Zvire
.
Metoda projev
navíc využívá vlastnosti zvané polymorfizmus – vyhodnocení metody zvuk
je ponecháno na definici odvozené třídy. Přímé zavolání abstraktní metody zvuk
v třídě Zvire
považujeme za chybu aplikace.
Chceme-li přímo zavolat nějakou metodu z nadřazené třídy, prohledávání můžeme vyvolat ručně pomocí prefixu SUPER::
. Dejme tomu, že třída Pes
bude mít další atribut a potřebujeme se s tím vyrovnat jak v konstruktoru, tak v metodě projev
.
package Pes; our @ISA = qw/Zvire/; sub new { my $trida = shift; my $self = $trida->SUPER::new(shift); $self->{kocka} = shift; return $self; } sub zvuk { return "Haf"; } sub projev { my $self = shift; my $projev = $self->SUPER::projev(); if (ref $self and ref $self->{kocka}) { $projev .= ' na kocku jmenem ' . $self->{kocka}->{jmeno}; } else { $projev .= ', nejlepe na kocky'; } return $projev; } package main; my $pes = Pes->new("Jonatan", Kocka->new("Micka")); print $pes->projev(), qq/\n/; print Pes->projev(), qq/\n/;
Nový konstruktor nejprve zavolá zděděný konstruktor ze třídy Zvire
(který objekt přiřadí do správné třídy – viz výše), a posléze k němu přidá svá data (atribut kocka
). Obdobně pracuje metoda projev
, která i v tomto případě umí jak volání přes objekt, tak přes celou třídu. Výstup z tohoto příkladu bude
Jonatan dela Haf na kocku jmenem Micka Zvirata typu Pes delaji Haf, nejlepe na kocky
Třída UNIVERSAL
Jak bylo řečeno, v hierarchii tříd stojí vždy nejvýše třída
UNIVERSAL
– jakási záchranná síť, pokud volanou metodu neimplementuje žádná jiná prohledávaná třída. Častým příkladem je vytvoření univerzální klonovací metody pomocí Data::Dumper
, nebo Storable
. Takovéto praktiky se však nedoporučují, jelikož si musíme uvědomit, že zavedením vlastní metody do třídy UNIVERSAL
měníme chování všech tříd, včetně natažených modulů s objektovým rozhraním. Je tudíž skoro vždy lepší si zadefinovat vlastní super-třídu a další třídy od ní odvodit. (V našem příkladu může být super-třídou například
Zvire
.)
Třída UNIVERSAL
nicméně obsahuje dvě užitečné metody, které jsou díky tomu automaticky přístupné ze všech tříd: isa
a can
.
Metoda isa('Trida')
vrátí pravdivou hodnotu pokud zavolaná třída nebo objekt je potomkem třídy Trida
(nebo přímo tou třídou).
print $pes->isa('Zvire'); # 1 print $pes->isa('Robot'); # 0
Metoda can('metoda')
funguje podobně. Pokud zavolaná třída nebo objekt „umí“ metodu metoda
, vrátí referenci na ni. (A to i pokud je metoda
pouze zděděná.) Pokud takovou metodu zavolat nelze, vrací se undef
.
if ($kocka->can('projev')) { print $kocka->projev(), qq/\n/; } # Nebo if (my $sub = $kocka->can('projev')) { print $kocka->$sub(), qq/\n/; }
Všimněte si, že $sub
voláme jako $kocka->$sub()
a nikoliv jako $sub->()
. Druhé zavolání by bylo špatně, jelikož by volaný podprogram nedostal objekt jako první parametr. Museli bychom explicitně zavolat $sub->($kocka)
.
Destruktory
Destrukce objektu se provádí v souladu s pravidly destrukce jakéhokoli zdroje, tj. klesne-li počet referencí na objekt na nulu. U objektu se navíc zavolá metoda DESTROY
, pomocí které můžeme dodatečně uvolnit prostředky, které objekt používá. Naše objekty obsahují pouze základní datové struktury a jednoduché reference bez cyklů, dealokace se tedy provede automaticky správně. Můžeme ale zavést ilustrativní konstruktory a destruktory, pro lepší pochopení životního cyklu našich zvířat.
package Zvire; use Carp; sub new { my $self = bless {}, shift; $self->{jmeno} = shift; print 'Zvire jmenem ', $self->{jmeno}, ' zacalo svou zivotni pout', qq/\n/; return $self; } sub DESTROY { my $self = shift; croak "Destruktor zavolan jako metoda tridy" unless ref $self; print 'Zvire jmenem ', $self->{jmeno}, ' odeslo do vecnych lovist', qq/\n/; } # … package Pes; our @ISA = qw/Zvire/; sub new { my $trida = shift; my $self = $trida->SUPER::new(shift); $self->{kocka} = shift; print 'Pes jmenem ', $self->{jmeno}; if (ref $self->{kocka}) { print ' zameril kocku jmenem ', $self->{kocka}->{jmeno}; } else { print ' nema kolem sebe zadnou kocku'; } print qq/\n/; return $self; } sub DESTROY { my $self = shift; if (ref $self and ref $self->{kocka}) { print 'Pes jmenem ', $self->{jmeno}, ' opustil pronasledovani kocky jmenem ', $self->{kocka}->{jmeno}, qq/\n/; } $self->SUPER::DESTROY(); } # … package main; my $pes = Pes->new("Jonatan", Kocka->new("Micka")); print $pes->projev(), qq/\n/; { my $rex = Pes->new("Rex"); print $rex->projev(), qq/\n/; }
Zde stojí za povšimnutí dvě věci. Za prvé, destruktor nemá smysl volat jako metodu třídy, takže takovouto akci ošetřujeme jako chybu programu. Za druhé, postupná destrukce objektu přes nadřazené třídy by se obvykle měla provádět v opačném pořadí než jeho konstrukce. Výstup našeho malého „animal planet kanálu“ vypadá takto:
Zvire jmenem Micka zacalo svou zivotni pout Zvire jmenem Jonatan zacalo svou zivotni pout Pes jmenem Jonatan zameril kocku jmenem Micka Jonatan dela Haf na kocku jmenem Micka Zvire jmenem Rex zacalo svou zivotni pout Pes jmenem Rex nema kolem sebe zadnou kocku Rex dela Haf, nejlepe na kocky Zvire jmenem Rex odeslo do vecnych lovist Pes jmenem Jonatan opustil pronasledovani kocky jmenem Micka Zvire jmenem Jonatan odeslo do vecnych lovist Zvire jmenem Micka odeslo do vecnych lovist
Závěr
Dnes jsme si ukázali principy, kterými Perl implementuje vlastnosti OOP. Tyto principy jsou stejné, ať používáme klasické hashové, nebo moderní inside-out objekty, o kterých si povíme příště. Pro další experimenty lze použít souhrnný kód vytvořený v článku.