Perličky: objekty naruby

17. 4. 2008
Doba čtení: 6 minut

Sdílet

Inside-out objekty v programovacím jazyce Perl řeší několik notoricky známých problémů s klasickými objekty. Mezi tyto problémy patří zejména zapouzdřenost a nedostatečná ochrana proti překlepům. Své jméno si vskutku zaslouží, neboť obracejí naruby pohled na objekty. Jak přesně se s nimi pracuje?

Problémy klasických objektů

Klasické Perlovské objekty, popsané v minulém díle seriálu, používají reference na hashe, přičemž tento hash obsahuje kompletní informace o objektu, tj. obvykle jeho atributy. Jelikož můžeme kdykoliv referenci dereferencovat, jsou takto všechny informace v režimu „public“, tj. kdokoliv může atributy přímo číst nebo nastavovat. Co více, můžeme atributy i  mazat nebo přidávat. Například objekt třídy Pes z minulého dílu můžeme „trápit“ takto:

# Dejme tomu, že 'jmeno' má být pouze pro čtení, ale nikdo nám nezabrání
$pes->{jmeno} = 'Zmenene jmeno';

# Či ještě hůře
delete $pes->{jmeno};

# Nebo
$pes->{jmno} = 'Nove jmeno';     # Překlepem definujeme nový
                    # atribut, implementace
                    # s ním ale nepracuje

# Dokonce můžeme změnit představu o tom, koho pes pronásleduje
$pes->{kocka} = { neco => 'zleho' };

Tento přístup může být vhodný pro objekty, kterých je jeden kus z každé třídy, pro školní příklady, ale nikoliv pro praktické použití. Špatné hodnoty můžeme odfiltrovat (alespoň interně) při každém jejich použití, případně můžeme použít nějakou magii kolem Tie, aby se nám do objektu nesmyslné hodnoty vůbec nedostaly.

Silnějším argumentem proti je nedostatečné zapouzdření objektu. Pokud někdo „natvrdo“ nastavuje atribut jmeno na řetězec, nemůžeme už nikdy změnit ani implementaci, ani význam tohoto atributu. Samozřejmě můžeme použít funkce typu get_jmenoset_jmeno, nebo univerzální funkci jmeno typu get-set. Bohužel použití těchto funkcí je stále pouze alternativa k přímému přístupu k atributům a výsledek závisí jen na disciplíně programátora.

Zapouzdřenost pomocí uzávěrů

Vzpomeňme si, že privátní proměnnou lze vytvořit elegantně pomocí uzávěru, například konstrukcí typu

sub nova_hodnota {
    my $hodnota;

    return sub {
        $hodnota = shift if scalar(@_) > 0;
        return $hodnota;
    }
}
my $get_set_hod1 = nova_hodnota();
$get_set_hod1->(3);
print $get_set_hod1->() . qq/\n/;

Obsah proměnné $hodnota zůstává v paměti i po návratu z  nova_hodnota, alespoň dokud je možné s ním manipulovat pomocí vrácené funkce get_set_hod1. Jelikož jméno $hodnota neexistuje mimo funkci nova_hodnota, je možné s touto proměnnou manipulovat pouze pomocí get_set_hod1. Taneček kolem referencí na anonymní funkce lze odstranit, pokud použijeme jiný způsob, jak lexikálně oddělit $hodnota od zbytku kódu, například pomocí bloku kódu.

{
    my $hodnota;

    sub get_set_hodnota {
        $hodnota = shift if scalar(@_) > 0;
        return $hodnota;
    }
}
get_set_hodnota(3);
print get_set_hodnota() . qq/\n/;

Od uzávěrů k inside-out objektům

Poslední logický krok, který musíme udělat, je způsob, jak v jedné zapouzdřené proměnné udržovat data vícera objektů. To je vlastně důvod, odkud mají inside-out objekty své jméno. V modulu si uchováváme jeden hash pro každý atribut, který pak indexujeme pomocí unikátní hodnoty pro každý objekt.

Touto unikátní hodnotou bývá zpravidla adresa nějakého anonymního skaláru v paměti. To znamená, že ve vnějším světě objekt existuje pouze jako reference na skalár, jehož konkrétní hodnota je dokonce nepodstatná. Podstatná je pouze jeho adresa, tu však programátor nemůže měnit. Podle této adresy v metodách pak identifikujeme, s jakým objektem pracujeme.

package Zvire;
use Scalar::Util qw/refaddr/;

{
        my %jmena;
        sub new {
                my $skalar;
                my $self = bless \$skalar, shift;
                $jmena{refaddr $self} = shift;
                return $self;
        }
        sub get_jmeno {
                my $self = refaddr(shift);
                return $jmena{$self};
        }
        sub DESTROY {
                my $self = refaddr(shift);
                delete $jmena{$self};
        }
}

package main;

my $brouk = Zvire->new('Pytlik');
print 'Zdravi te ' . $brouk->get_jmeno() . qq/\n/;

Jak je patrné, po inicializaci objektu již nelze nijakým způsobem (mimo package Zvire) změnit atribut jmeno a nelze k němu ani přistupovat jinak, než pomocí get_jmeno, což je zapouzdření, které jsme potřebovali.

Ve funkci new možná zaujme konstrukce, kterou jsme získali referenci na anonymní skalár. Jelikož v Perlu neexistuje syntaktická konstrukce pro anonymní skaláry (podobně jako je např. [] pro pole), různí autoři si dopomáhají různými způsoby, včetně „idiomu“ $ref = \do { my $anon_scalar }. Jelikož záleží pouze na adrese, bylo by také možno použít referenci na pole nebo cokoliv jiného, nicméně cokoliv jiného než skalár zabírá zbytečně mnoho paměti.

Výhody inside-out objektů

Pokud namísto $jmena{$self} napíšeme například $jmeno{$self}, budeme při zapnutém use strict informováni, že taková proměnná neexistuje, a program bude ukončen. Na rozdíl od toho, u běžných (hashových) objektů jeden překlep v tichosti vytvoří nesprávný klíč v hashi (kvůli vlastnosti autovivification).

Paměťovou náročnost nad rámec uchovaných dat (overhead) hashových objektů můžeme odhadnout jako N*H, kde N je počet objektů a H je overhead jednoho hashe. Náročnost inside-out objektů pak odhadneme jako A*H + N*S, kde A je počet atributů a S je overhead skaláru. Jelikož S < H, je patrné, že při dostatečně velkém N bude overhead inside-out objektů menší. V praxi se ukazuje, že stačí přibližně N > 1.15*A, tj. o 15% více objektů než atributů v jednom objektu.

U klasických hashových objektů hrozilo při použití dědičnosti nebezpečí, že různé specializace třídy mohly přepisovat (omylem či úmyslně) atributy rodičovské třídy. Inside-out třída má atributy uchovány v lexikálních proměnných, do kterých není přístup odjinud než z modulu této konkrétní třídy. Specializace této třídy může ve svém modulu použít stejnou proměnnou pro úplně jiné atributy, aniž by došlo ke kolizi.

Nevýhody inside-out objektů

Povšimněme si nutnosti uvést explicitní desktruktor DESTROY. Pokud zanikne anonymní skalár, jehož adresou indexujeme atributový hash, nemá se Perl jak dovědět, že příslušná položka v hashi již není smysluplná. Musíme tedy výmaz provést manuálně.

Dalším problémem, z podobného důvodu, je serializace objektu. U klasického objektu je automaticky serializován hash a všechny jeho položky. U inside-out objektu je potřeba napsat vlastní funkce, které serializují příslušné položky v hashi atributů.

Dále je tu podpora vícevláknového zpracování. Při vytvoření nového vlákna se obvykle mění adresy objektů, a jelikož používáme adresu jako klíč v hashi, budou obvykle příslušná data nedostupná. Obvyklým řešením je metoda CLONE, která příslušné adresy poopraví. Tato metoda ale není úplně triviální, navíc je potřeba si zvlášť pamatovat adresy všech existujících objektů.

Nakonec je tu nutnost často používat refaddr. Bez jeho použití bude reference převedena na řetězec (něco jako "Zvire=SCALAR(0x623c20)"), což očividně může vést k nepříjemným chybám při opomenutí na  refaddr.

Tyto problémy jsou obvykle řešeny v modulech pro automatizaci vytváření tříd, tak aby vzniklá třída „prostě fungovala“. V nové verzi Perlu, 5.10, většinu problémů řeší automaticky takzvané fieldhashe.

Modul Hash::Util::FieldHash

Fieldhashe byly vytvořeny zejména pro odstranění nedostatků inside-out objektů přímo v interpretu jazyka Perl. Při použití fieldhashů je automaticky hlídána většina věcí, z uvedených problémů je prakticky potřeba starat se pouze o serializaci.

package Zvire;
use Hash::Util::FieldHash qw/fieldhash/;

{
        fieldhash my %jmena;
        sub new {
                my $skalar;
                my $self = bless \$skalar, shift;
                $jmena{$self} = shift;
                return $self;
        }
        sub get_jmeno {
                my $self = shift;
                return $jmena{$self};
        }
}

package main;

my $brouk = Zvire->new('Pytlik');
print 'Zdravi te ' . $brouk->get_jmeno() . qq/\n/;

Fieldhash definujeme pomocí funkce fieldhash, pokud bychom chtěli více hashů najednou, použijeme fieldhashes \ my (%hash1, %hash2) (je potřebné použít reference, jinak by se nám hashe splaskly do jednoho). Fieldhash předpokládá, že budeme jako klíče používat adresy objektů, tudíž nemusíme explicitně používat refaddr. Dále si fieldhash adresu zapamatuje a při zaniknutí objektu na této adrese provede automaticky garbage collection příslušné hodnoty v hashi. Problémy s klonováním při využití více vláken řeší podobně.

Modul nabízí ještě dvě „slabší“ varianty hashe: idhashe a  funkci register. Idhashe se chovají jako normální hashe (tj. bez automatického mazání prvků), ale u každého klíče je předpokládáno, že se jedná o referenci a jako skutečný klíč je použita adresa referencovaného objektu (tj. automaticky se volá funkce id).

Funkci register lze použít na klíč běžného hashe (nebo idhashe) a „zaregistrovat“ tak tento klíč pro automatické vymazání. Fieldhashe spojují jak idhashe, tak funkci register do jednoho celku, nicméně tato částečná řešení se můžou hodit, například pokud v hashi uchováváme ještě něco jiného.

ict ve školství 24

Konečně, zmíněná funkce id slouží víceméně jako zkratka k původní funkci refaddr z modulu Util::Scalar.

Závěr

Dnes jsme se seznámili s moderní metodou implementace objektů v Perlu – objekty inside-out, a to spíše po teoretické stránce. Příští díl bude věnován několika praktickým modulům, které umožňují automatické vybudování tříd.

Autor článku