Hlavní navigace

Perličky: principy objektového programování

4. 4. 2008
Doba čtení: 10 minut

Sdílet

Objektové programování je módou dnešní doby. Pravda, v některých oblastech až přehnaně, ale i tak je to zajímavý pohled na řešení problémů a má své místo na světě. Jako spousta dalších funkcí, bylo i objektové programování doděláno do jazyka Perl až v průběhu jeho života. Pojďme se podívat, jak.

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 – PesKocka. 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 necoparametry 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 PesKocka 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: isacan.

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.

root_podpora

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.

Byl pro vás článek přínosný?