Objektově orientované programování

Základy objektově orientovaného programování

Objektově orientované programování má své počátky na konci 70. let minulého století. Do té doby byl používán strukturovaný přístup a jednotlivé problémy se dekomponovaly na menší celky až po nejnižší úroveň (metoda shora dolů). Naproti tomu objektový přístup funguje na principu zdola nahoru, kdy jsou definovány základní stavební prvky a z nich je teprve celý program skládán. Díky tomu je možné tyto prvky opětovně použít, stačí jen zajistit jejich společnou existenci a komunikaci s ostatními. Jedná se v podstatě o modelování reálné skutečnosti. Objekty jsou potom obrazem objektů skutečného světa. Mají své vlastnosti a mohou vykonávat určité činnosti. Každý objekt je instancí nějaké třídy objektů. Ta definuje množinu činností, které mají všechny objekty této třídy společné. Jednotlivé objekty stejné třídy se od sebe tedy liší pouze vlastnostmi. Tyto vlastnosti se nazývají atributy objektů. Činnosti, které objekty mohou provádět se nazývají metody. Důležité je to, že každý objekt přesně ví, ke které třídě náleží.

Vlastnosti OOP

Typy metod

Rozlišujeme mezi dvěma typy metod. První typ jsou metody objektů, což jsou metody vztahující se vždy k jednomu konkrétnímu objektu. Druhým typem jsou metody tříd. To jsou metody, které nepatří žádnému objektu, ale jsou společné pro všechny objekty dané třídy. Typickým představitelem metody třídy je konstruktor. Jedná se o zvláštní metodu, která vytváří konkrétní nový objekt, novou instanci dané třídy. Konstruktor může být také metodou objektu a to v případě, že je nový objekt vytvářen např. podle vlastností konrétního daného objektu. Vytváří-li se nový objekt jako kopie stávajího objektu, hovoříme o kopírovacím konstruktoru. Dalším zvláštním typem metody je destruktor. Jedná se o metodu, která po ukončení existence objektu provede jeho zrušení. Je to metoda prováděná jako poslední činnost objektu v rámci jeho existence.

Implementace objektů v Perlu

Na rozdíl od některých jiných programovacích jazyků Perl neposkytuje nový datový typ pro implementaci objektů, ale maximálně využívá stávajících prostředků.

Objekty

Zatímco v jiných programovacích jazycích je přesně definovaný počet a typ atributů objektu, v Perlu tomu tak není. Definice atributů tedy není součástí definice třídy objektů. Objekt jakékoliv třídy může mít obecně žádný, jeden nebo více atributů (ve skutečnosti aspoň jeden obvykle mívá). Máme-li uchovat obecně několik hodnot, zvolíme seznamovou datovou strukturu -- pole nebo hash. Jak bylo zmíněno v kapitole Složitější datové struktury, asi nejvhodnějším prostředkem je hash. Může obsahovat libovolné množství hodnot, pomocí řetězcových klíčů je možné k nim srozumitelně přistupovat, je možné jednoduše hodnoty libovolně přidávat a odebírat. Ve skutečnosti je použití hashů pro uchování atributů nejčastěji se vyskytujícím způsobem. Objekty jsou tedy datové struktury, které obsahují atributy a které vědí, do jaké objektové třídy patří. Protože se může jednat o libovolnou datovou strukturu pracuje se s odkazy na tuto strukturu. Pomocí jednoho skaláru a odkazů je totiž možné uchovávat množství dat v rozsáhlých atrukturách. Objekt je tedy odkazovaný obsah a pro práci s objektem se používá reference odkazující na tento obsah.

Metody

Metody objektů jsou obyčejné podprogramy. Mohou přijímat seznam argumentů, mohou vracet seznam hodnot, často v nich probíhá modifikace vlastností objektů. Podprogramy se nikdy nevztahují pouze k některým datům, ale jsou globální, proto je jim třeba říci, s jakým konkrétním objektem mají pracovat. Proto je vždy prvním argumentem tohoto podprogramu odkaz na určitý objekt (metoda objektu) nebo jméno objektové třídy (metoda třídy).

Třídy objektů

Jak bylo řečeno v úvodní části, atributy objektů a metody spolu tvoří logický celek, který s okolím komunikuje prostřednictvím rozhranní. Způsobem, jakým v Perlu zajistit logickou souvislost určité části kódu s možností poskytnutí rozhranní a možností znovupoužitelnosti, jsou balíky, případně moduly. Třída objektů je tedy balík, kde jsou definovány metody. Tím, že objekt ví, z jaké třídy byl odvozen, ví také, z jakého balíku má volat podprogramy.

Aby bylo možné objektové třídy opětovně použít, je vhodné umístit je do souboru a ten potom pomocí use nebo require vtáhnout na příslušné místo. Modul může poskytovat buď standardní rozhraní pomocí exportu a importu symbolů nebo objektové rozhraní pomocí definice objektové třídy a používání metod této třídy. Oba typy rozhraní modulu je samozřejmě možné kombinovat dohromady.

Vytvoření objektu, konstruktory

Objekt je datová struktura, na niž si udržujeme odkaz. Odkazovanému odkazu je třeba říci, k jaké třídě (neboli k jakému balíku) náleží. Je to proto, abychom mohli volat metody objektu. K tomu slouží vestavěná funkce bless. Ta jako první argument přijímá odkaz a tomuto odkazu řekne, že to, na co odkazuje, patří do balíku, jehož jméno ja zadáno jako druhý argument. Je-li tento argument vynechán, použije se jméno aktuálního balíku.

$objekt = {};
$trida = 'Trida';
bless $objekt, $trida;
# nebo bless $objekt, 'Trida';

Když potom zavoláme funkci ref s argumentem $objekt, získáme název třídy, do které objekt náleží, místo řetězce HASH.

Metody, které slouží k vytváření objektů se nazývají konstruktory. Objekty jsou nejčastěji získány jako návratové hodnoty těchto funkcí. Tyto metody obvykle pracují tak, že alokují nový prostor pro ukládání atributů, označí jeho příslušnost ke třídě a vrátí odkaz na takto alokovaný prostor.

Konstuktory bývají často pojmenovány new. V případě, že konstruktor bude volán jako metoda třídy, může jeho implementace vypadat následovně:

sub new {
    my $trida = shift;
    my $objekt = {};
    bless $objekt, $trida;
    return $objekt;

    # nebo zkáceně
    # sub new { bless {}, shift }
}

Použití druhého argumentu je vhodné v případě, že chceme, aby konstruktor dědily odvozené třídy.

Konstruktor jako metoda objektu.

sub new {
    my $trida = ref shift;
    my $objekt = {};
    bless $objekt, $trida;
    return $objekt;
}

Konstruktor jako metoda třídy i metoda objektu.

sub new {
    my $kdo_vola = shift;
    # v $kdo_vola je buď jméno třídy nebo odkaz na objekt
    
    my $trida;
    if (ref $kdo_vola) {
        # je to objekt
        $trida = ref $kdo_vola;
    } else {
        # je to jméno třídy
        $trida = $kdo_vola;
    }
    # nebo zkráceně $trida = ref $kdo_vola || $kdo_vola

    my $objekt = {};
    bless $objekt, $trida;
    return $objekt;
}

Tento princip obecně platí pro jakoukoliv metodu v případě, že chceme rozlišit případy, kdy je metoda volána jako metody objektu nebo třídy.

Nastavení hodnot atributů

Chceme-li ihned v okamžiku vytváření objektu nastavit hodnoty atributů, můžeme tak v konstruktoru učinit. V předchozích případech byl objekt vytvořen s prázdným seznamem atributů (vraceli jsme odkaz na prázdný hash). Provedeme-li inicizlizaci hodnot tohoto hashe, nastavíme tak hodnoty atributů v okamžiku vytvoření tohoto hashe.

sub new {
    # vytvoření nového auta typu formule
    
    my $kdo_vola = shift;
    my $trida = ref $kdo_vola || $kdo_vola;
    return bless {-pocet_mist => 1}, $trida;
}

V tomto případě byl v okamžiku vytváření objektu nastaven atribut -pocet_mist na hodnotu 1. Seznam atributů a jejich hodnot samozřejmě nemusí být znám již v případě definice konstruktoru, ale může být předán jako seznam argumentů konstruktoru.

sub new {
    my $kdo_vola = shift;
    my $trida = ref $kdo_vola || $kdo_vola;
    return bless {@_}, $trida;
}

Případně mohou být oba způsoby zkombinovány dohromady:

sub new {
    # vytvoření nového auta typu formule
    
    my $kdo_vola = shift;
    my $trida = ref $kdo_vola || $kdo_vola;
    return bless {-pocet_mist => 1, @_}, $trida;
}

Jiný způsob inicializace atributů.

sub new {
    my $kdo_vola = shift;
    my $trida = ref $kdo_vola || $kdo_vola;
    my $objekt = {@_}
    bless $objekt, $trida;
    $objekt->inicializuj_atributy;
    return $objekt;
}
sub inicializuj_atributy {
   # počáteční nastavení atributů pro auto typu formule
    my $objekt = shift;
    $objekt->{-pocet_mist} = 1;
}

Při vytváření objektu (tzn. odkazovaného obsahu) by se měl použít způsob, kdy je při každém vytvoření objektu alokován nový prostor.

package Trida;
sub new {
    # při vytvoření objektu nastavíme atribut
    # na hodnotu 1
    %objekt = (atribut => 1);
    bless \%objekt, shift;
}
# vytvoříme dva objekty
my $o1 = new Trida;
my $o2 = new Trida;

# vypíšeme jejich atributy
print $o1->{atribut};  # vytiskne '1'
print $o2->{atribut};  # vytiskne '1'
    
# změníme hodnotu atributu prvního objektu
$o1->{atribut} = 2;

# vypíšeme jejich atributy
print $o1->{atribut};  # vytiskne '2'
print $o2->{atribut};  # vytiskne '2'

Problém je v tom, že oba objekty jsou tvořeny tím stejným prostorem v paměti. Řešením je použití konstruktoru anonymního hashe nebo lexikální vymezení proměnné určené pro alokaci prostoru pro objekt.

sub new {
    my %objekt = (atribut => 1);
    bless \%objekt, shift;
}
# nebo
sub new {
    $objekt = {atribut => 1};
    bless $objekt, shift;
}
# nebo
sub new {
    my $objekt = {atribut => 1};
    bless $objekt, shift;
}
# nebo
sub new {
    bless {atribut => 1}, shift;
}

Metody

Metody objektů se poněkud liší od běžných podprogramů. Není to ani tak způsobem jejich definice, ale způsobem práce s nimi. Běžných podprogramy jsou normálně provedeny a již v době překladu je jasné, o jaký podprogram z jakého balíku se jedná. U metody je tato skutečnost známá až v okamžiku jejího volání. To je důvod, proč u metod objektů nefunguje mechanismus prototypů. Ten totiž probíhá již v době překladu, ale v tomto případě ještě není jasné, jaký podprogram se volá.

To, do kterého balíku metoda náleží, se určí podle toho, kdo ji zavolal. Jedná-li se o metody objektu, metodu zavolal konkrétní objekt, je-li to metoda třídy, je volání provedeno třídou samotnou (balík, do kterého metoda náleží, se určí ze jména třídy). Skutečnost, kdo metodu volá, je známa až v okamžiku jejího volání. Díky tomu je volání metody o něco pomalejší, než volání obyčejného podprogramu.

Voláme-li metody třídy, je snadné určit balík, do kterého podprogram náleží. O něco komplikovanější je to v případě, že voláme metodu objektu. Při vytváření je objektu odkazovanému obsahu pomocí funkce bless řečeno, k jaké třídě náleží, a potom je možné pomocí funkce ref název této třídy získat.

$m = new Modul;   # vytvoření objektu
print ref $m;     # vypíšeme třídu objektu,
                  # vytiskne 'Modul'

Existují dva druhy volání metod.

Volání metod pomocí operátoru ->

Tento způsob volání je převzatý z jiných programovacích jazyků. Na levé straně operátoru se nalézá ten, co chce metodu vyvolat, a na pravé straně jméno metody. Je-li nalevo objekt, jedná se o volání metody objektu, je-li tam jméno třídy, jedná se o volání metody třídy. Případný seznam argumentů metody musí být uzavřený v kulatých závorkách.

$objekt->metoda;           # metoda objektu
$objekt->metoda(1, 2, 3);  # metoda objektu
                           # s argumenty
Trida->metoda;             # metoda třídy
Trida->metoda(1, 2, 3);    # metoda třídy
                           # s argumenty

Je automaticky zajištěno, že v případě volání metody třídy je jako první argument funkci předáno jméno třídy a že v případě volání metody objektu je jako první argument předán odkaz na objekt. Konkrétní naložení s tímto argumentem už je dáno implementací konkrétní funkce.

Jméno funkce může být určeno i pomocí skalární proměnné s využitím mechanizmů symbolických odkazů.

$objekt->$jmeno_metody(@argumenty);

Proměnná $jmeno_metody obsahuje řetězec, který bude interpretovaán jako jméno volané metody. Ani při použití striktního režimu nebude tento způsob volání považován za chybu.

Volání metod pomocí nepřímé notace

Toto volání se neliší od volání obyčejných podprogramů v Perlu. Nejprve je uvedeno jméno funkce, pak následuje ten, kdo metodu volá (jméno třídy nebo odkaz na objekt), a nakonec případný seznam argumentů. Vyvolavatel metody není od seznamu argumentů oddělený čárkou podobně jako v případě použití jména ovladače u funkce print. Kulaté závorky kolem seznamu argumentů jsou nepovinné stejně jako u seznamových operátorů (je třeba se pouze řídit prioritou operátorů ve výrazu, kde metody voláme).

metoda $objekt;           # metoda objektu
metoda $objekt 1, 2, 3;   # metoda objektu
metoda $objekt (1, 2, 3); # s argumenty
metoda Trida;             # metoda třídy
metoda Trida 1, 2, 3;     # metoda třídy
metoda Trida (1, 2, 3);   # s argumenty

Na místě vyvolavatele metody se může nacházet blok, jehož návratová hodnota bude použita pro určení objektu nebo jména třídy.

Nachází-li se na tomto místě něco složitějšího, než jméno, jednoduchý skalár nebo blok, jedná se pravděpodobně o chybu. Platí zde stejná pravidla jako při použití odkazu ve jméně proměnné v případě dereference:

metoda1 $pole[0];
metoda2 $pole->[0];

je chápáno jako

$pole->metoda1([0]);
$pole->metoda2->[0];

Jednoznačné volání metody

V případě, že aktuální balík obsahuje jméno podprogramu, které je stejné jako jméno volané metody nebo jméno třídy, může nastat problém.

metoda Trida;   # může být chápáno jako metoda('Trida'),
                # metoda(Trida())
Trida->metoda;  # může být chápáno jako Trida()->metoda 

Aby se této dvojznačnosti zabránilo, je možné za jméno třídy umístit sybol ::

metoda Trida::;
Trida::->metoda;

V takovém případě budou oba zápisy vždy chápány jako volání metody. V případě, že neexistuje jméno podprogramu stejné se jménem třídy nebo byl-li zaveden modul stejného jména jako jméno třídy (pomocí use nebo require), k takové nejednoznačnosti nedojde.

Dědičnost

Perl pro realizaci dědičnosti neposkytuje žádný zvláštní způsob. Jediné, co lze dědit jsou metody, protože atributy objektu jsou tvořeny datovou strukturou, která je pro každý objekt jedinečná, není předem známá a je možné ji libovolně modifikovat. Metody jsou podprogramy, které náleží do balíku se stejným názvem, jaké je jméno třídy. V případě, kdy chceme, aby jedna třída zdědila vlastnosti jiné třídy, nedochází k přenesení definice funkcí do nového balíku nebo k něčemu podobnému. Nový balík obsahuje pouze funkce, které jsou v něm definované. Při dědění se tedy metody zdědené z rodičovské třídy nenacházejí v novém balíku, ale v balíku puvodním, a jsou přístupné třídám odvozeným. Označení rodičovských tříd je možné pomocí pole @ISA, které patří do balíku odvozené třídy. To obsahuje názvy balíků všech tříd, které jsou rodičovskými trídami pro aktuáloní třídu objektů.

Vyhledávání metody

@ISA = qw( Modul1 Modul2 );
# pokud neprovedeme use nebo require, žádný z balíků Modul1
# ani Modul2 nebudou dostupné a nebude v nich možné hledat
# definice metod

Třída UNIVERSAL

Třída UNIVERSAL je posledním místem, kde probíhá vyhledávání metod. Jedná se tedy o univerzálního předchůdce, který už sám žádné předchůdce nemá (není zde definováno pole @ISA). Poskytuje tři metody, které jsou tedy dostupné ze kterékoliv třídy. Volat tyto funkce může jak objekt tak třída a návratovými hodnotami jsou vlastnosti týkající se vyvolavatelů metody.

# můžeme volat způsobem
$objekt->metoda(...);
Trida->metoda(...);

Metoda can

if ($objekt->can('metoda')) {
   # $objekt může volat metodu 'metoda'
   $objekt->metoda;
}

Metoda isa

if ($objekt->isa('Trida')) {
   # $objekt je potomkem třídy 'Trida'
   print "Objekt je potomkem nebo objektem třídy Trida.";
}

Metoda VERSION

# vypíšeme verzi modulu a chceme, aby byla aspoň 1.5
print "Verze modulu: ", $objekt->VERSION(1.5);

Vyhledání metody předchůdce

Předefinujeme-li metodu svého předchůdce a chceme-li volat půdovní metodu, existují dvě možnosti, jak toho dosáhnout. Závisí to na tom, kde to chceme provést. V případě, že máma vytvořený objekt a chceme volat metodu předka tohoto objektu, musíme použít jméno metody včetně jména třídy, ze které metoda pochází.

$objekt->Rodic::metoda;  # pomocí operátoru ->
Rodic::metoda $objekt;   # pomocí nepřímé notace

Chceme-li metodu nadřazené třídy volat uvnitř definice metody odvozené třídy, můžeme opět pojmenovat nadřazenou třídu a volat metodu z této třídy.

sub metoda {
    my $objekt = shift;
    
    # nějaké akce nové pro tuto třídu
    # ...
    
    # volání konkrétní rodičovské metody
    $objekt->Rodic::metoda(@_);
}

V případě, že nechceme jmenovat konkrétní třídu, můžeme použít použít pseudotřídu SUPER. Díky ní dochází k postupnému procházení balíků v poli @ISA stejně jako při hledání metody, a až je metoda nalezena, provede se.

sub metoda {
    my $objekt = shift;
    
    # nějaké akce nové pro tuto třídu
    # ...
    
    # volání první nalezené rodičovské metody
    $objekt->SUPER::metoda(@_);
}

Pomocí pseudotřídy SUPER se prochází vždy pouze pole @ISA definované v aktuálním balíku, kde je funkce překládána. Většinou je všechno v pořádku, problém by mohl nastat, když definujeme funkci v jednom balíku a přitom se nacházíme v jiném balíku.

Zajištění soukromí

Každý objekt je tvořen atributy a metodami. Atributy jsou dostupné prostřednictvím odkazu na objekt, stejně tak jako metody. Protože jsou metody podprogramy, jsou navíc dostupné i pomocí tabulky symbolů. Chceme-li zajistit, aby některá data a metody byly privátní a nebyly dostupné zvenčí, musíme zajistit, aby se nenacházely v tabulce symbolů. Tento případ se týká především metod, protože atributy jako součást jedné datové struktury privátní být nemohou. Můžeme ale ukrýt některá pomocná data, která nechceme prezentovat navenek.

Řešením je umístit příslušné proměnné obsahující privátní data do lexikálních prostorů. Podprogramy se v tabulce symbolů nacházejí vždy, proto musíme zvolit cestu s použitím odkazů na podprogramy. Ty jsou skalární hodnoty a je možné je do lexikálních prostorů umístit. Uvnitř balíku popisujícího třídu objektů je pak můžeme volat pomocí dereference.

package Trida;

my $pomocna_data;
my $skryta_metoda = sub {
    # definice metody
    # ...
}

sub metoda {
   my $objekt = shift;
   
   # voláme metodu zpracovácající pomocná data
   $objekt->zpracuj($pomocna_data);

   # volání skryté metody
   &$skryta_metoda(@argumenty);
}

V tomto případě je funkce odkazovaná proměnnou $skryta_metoda volaná jako obyčejné funkce. Pokud chceme, aby se chovala jako pravá metoda, tzn. aby jako první argument očekávala odkaz na objekt, můžeme to provést dvěma způsoby - zavoláme ji jako metodu buď pomocí nepřímé notace nebo s využitím operátoru -> s použítím odkazu na podprogram.

&$skryta_metoda $objekt, @argumenty;
$objekt->$skryta_metoda(@argumenty);

Takto definované skryté metody není možné volat zvenčí a ani nejsou děděny.

Zrušení objektu, destruktor

Objekt je odkazovaný obsah a je s ním zacházeno jako s každým jiným odkazem - po jeho posledním použití je alokovaný prostor uvolněn. V tomto okamžiku je volána funkce zvaná destruktor objektu. V Perlu má standardní název DESTROY. Definice této funkce se nachází v balíku, kde je definovaná třída objektu. Pokud se tam nenachází, není destruktor volán. Většinou destruktory potřeba nejsou, protože je většina věcí dělána automaticky, ale je zde možné provést některé akce, které automaticky provést nejdou. Takovými činnostmi mohou být odpojení od databáze, zápis do souboru o ukončení používání objektu a jeho uzavření, poslání e-mailu s informacemi nashromážděnými za dob existence objektu apod. Dektruktor je také vhodným místem pro rozbití případných kruhových odkazů, aby mohlo dojít k uvolnění paměti v procesu garbage collection.

© 2004, František Dařena

Valid XHTML 1.0! Valid CSS!