Class::InsideOut
Modul Class::InsideOut je minimalistický generátor tříd typu inside-out. Základní použití je
use strict; use warnings; package Zvire; use Class::InsideOut qw/:all/;
Pomocí tagu :all importujeme do třídy mimo jiné i minimalistický konstruktor new. Tento konstruktor vyřeší registraci atributových hashů pro automatickou dealokaci a naplnění atributů pomocí parametrů zavolané funkce new. Naplní se pouze ty atributy, které existují, zbytek se ignoruje. Samotné atributy definujeme pomocí slov public, readonly a private.
readonly jmeno => my %jmena;
Tímto říkáme, že hash %jmena obsahuje atribut
jmeno a zároveň definujeme implicitní accessor jmeno(). Tento accessor je univerzálního typu get-set, tzn. volání bez argumentu dodá aktuální hodnotu a volání s argumentem hodnotu nastaví. V případě readonly volání s parametrem selže, v případě private se accessory nevytváří. Pomocí private je tedy možné také vytvářet vlastní accessory.
A to je vše, co se týče třídy Zvire. Odvozenou třídu Zvire::Kocka vytvoříme obdobně za použití klasického use base nebo our @ISA = qw/Zvire/.
package Zvire::Kocka; use Class::InsideOut qw/:all/; use base qw/Zvire/;
V odvozené třídě Zvire::Pes chceme definovat navíc atribut kocka s tím, že budeme vyžadovat, aby byl tento atribut buď undef, nebo instance třídy Zvire::Kocka. Kontrolu budeme provádět i v konstruktoru, a tudíž nestojíme o implicitní konstruktor. Použijeme tedy tag :std.
package Zvire::Pes;
use Class::InsideOut qw/:std/;
use Carp qw/croak/;
use base qw/Zvire/;
sub _check_kocka {
my $kocka = shift;
# parametr musi byt undef a nebo objekt tridy Zvire::Kocka
return if not defined $kocka;
unless ($kocka->isa('Zvire::Kocka')) {
die "ocekaval se objekt Zvire::Kocka\n";
}
}
Atribut kocka pak nadefinujeme tak, že při jeho změně je zavolána automaticky naše kontrolní funkce. To uděláme pomocí funkce set_hook. Tato do naší (buď anonymní nebo pojmenované) funkce předá v $_ to, co by se předalo accessoru. Proměnnou $_ je možné změnit, a v tom případě pak accessor použije tuto pozměněnou hodnotu. My ji měnit nebudeme, pouze zkontrolujeme platnost.
Uvnitř hooku je možné zavolat die. Modul Class::InsideOut za nás chytře převede kontext chyby tak, aby se v hlášce objevilo místo, odkud byl accessor zavolán, a nikoliv hook samotný.
public kocka => my %kocky, {
# pri zmene atributu je provedena jeho kontrola
set_hook => sub { _check_kocka($_); }
};
Posledním úkolem je předefinovat konstruktor new tak, aby prováděl kontrolu také. V tomto případě budeme muset kontext případného die vyřešit sami. K vlastní konstrukci je možno použít nejprve konstruktor z modulu Class::InsideOut a poté provést kontrolu již nahraných atributů. Pokud by toto z nějakého důvodu nestačilo, můžeme provést klasicky bless na anonymní skalár, ale ten poté musíme pomocí funkce register zaregistrovat k automatické dealokaci a klonování pro vícevláknové programy.
# inicializace s kontrolou
sub new {
my $self = Class::InsideOut::new(@_);
eval {
# zde je mozno provest nekolik kontrol
_check_kocka($kocky{id $self});
};
croak $@ if $@;
return $self;
}
Nakonec vyrobíme metodu, která nějakým způsobem využívá atributy objektu. Zde je obvykle možné použít již existující accessory. Pokud bychom se v hashích chtěli hrabat ručně, použijeme obvyklý způsob přes refaddr nebo zkráceně id.
sub pronasleduj {
my $self = shift;
print $self->jmeno()
, defined $self->kocka()
? ' pronasleduje kocku/kocoura jmenem '
. $self->kocka->jmeno()
: ' nepronasleduje nikoho'
, ".\n";
}
Demonstrační kód vyrobí dva objekty a použije accessor kocka na objektu třídy Zvire::Pes.
package main; my $tom = Zvire::Kocka->new(jmeno => 'Tom'); my $spike = Zvire::Pes->new(jmeno => 'Spike', kocka => $tom); $spike->pronasleduj(); $spike->kocka(undef); $spike->pronasleduj();
Pokud bychom chtěli dělat vylomeniny, Perl nás potrestá.
$spike->jmeno('ahoj');
jmeno() is read-only at cio.pl line 74
$spike->kocka('ahoj');
kocka() ocekaval se objekt Zvire::Kocka at cio.pl line 74
Nicméně, jak bylo řečeno, implicitní minimalistický konstruktor ignoruje nadbytečné argumenty, což může být vnímáno jako nerobustní.
my $prasopes = Zvire::Pes->new(jmeno => 'Spike', prase => 'kvik');
Milou vlastností této minimalistické verze generátoru tříd je mimo rychlost kompilace i zvládnutí tzv. black-box inheritance, tj. možnost odvodit svou třídu z jakékoliv třídy, třeba i takové, která je implementována „postaru“ klasickými blessovanými hashi.
Object::InsideOut
Navzdory podobnému názvu je tento modul spíše protipólem výše zmíněného Class::InsideOut. Jedná se o zbraněmi nabitého, supertěžkého bojového robota, o čemž svědčí mimo jiné i délka dokumentační stránky, kterou je také vhodné si přečíst. Na druhou stranu, o to kratší je pak náš kód.
use strict;
use warnings;
package Zvire;
{
use Object::InsideOut;
# atributy
my @jmeno :Field
:Get(get_jmeno)
:Arg(Name => 'jmeno', Mandatory => 1);
}
Na rozdíl od klasických tříd typu inside-out se zde používají místo hashů pole, která jsou pro přístup podstatně rychlejší. (V případě explicitní potřeby hashe je možno použít i hash.) Přístup k proměnné v případě pole se pak dělá přes dereferenci objektu, tj. $jmeno[$$self], ale, jako obvykle, ve většině případů je možno použít accessory.
Tag :Field říká, že proměnná bude sloužit jako atribut objektu, tj. obstará se automatická dealokace a tak dále.
Pomocí tagu :Get definujeme automatický accessor pro čtení hodnoty get_jmeno. Pomocí :Set můžeme vyrobit accessor pro zápis. Pomocí :Std(jmeno) zároveň vyrobíme oba. Někdo může chtít accessor typu get-set, v tom případě poslouží :Acc(jmeno). Navíc lze definovat pomocí :lv accessor tak, aby bylo možné jej použít na levé straně přiřazení.
Tag :Arg umožňuje konstruktoru (který je plně v režii Object::InsideOut) naplnit atribut ze svých parametrů. Navíc zde máme lepší kontrolu nad tím, co se použije jako parametr konstruktoru. Atributy označené jako Mandatory musí být vyplněny a neznámé atributy jsou vyhodnoceny jako chyba. Lze také specifikovat výchozí hodnotu pomocí Default a pomocí tagu :InitArgs provést námi dodanou dodatečnou inicializaci.
package Zvire::Kocka;
{
use Object::InsideOut qw/Zvire/;
}
Odvození třídy se provádí přímo přes Object::InsideOut, tedy nepotřebujeme žádné use base.
package Zvire::Pes;
{
use Object::InsideOut qw/Zvire/;
sub _check_kocka {
my $kocka = shift;
# parametr musi byt undef a nebo objekt tridy Zvire::Kocka
return 1 if not defined $kocka;
return $kocka->isa('Zvire::Kocka');
}
my @kocka :Field
:Arg(kocka)
:Type(\&Zvire::Pes::_check_kocka)
:Std(kocka);
U atributu kocka si necháme opět kontrolovat typ. Pomocí tagu :Type(Zvire::Kocka) bychom mohli požadovat, aby parametr byl instancí této třídy, nicméně tím bychom přišli o možnost undef. Proto použijeme referenci na vlastní kontrolovací funkci. Ta tentokrát nevolá die, ale pouze vrací hodnotu pravda/nepravda. Případné die pak zařídí Object::InsideOut.
Zbytek kódu se příliš nemění. Použití accessorů tedy zjevně naplňuje svůj smysl, tj. zapouzdření implementace třídy. Kdybychom chtěli být precizní, můžeme nadefinovat accessory jako get-set, v tom případě by se kód neměnil vůbec.
sub pronasleduj {
my $self = shift;
print $self->get_jmeno()
, defined $self->get_kocka()
? ' pronasleduje kocku/kocoura jmenem '
. $self->get_kocka()->get_jmeno()
: ' nepronasleduje nikoho'
, ".\n";
}
}
package main;
my $tom = Zvire::Kocka->new(jmeno => 'Tom');
my $spike = Zvire::Pes->new(jmeno => 'Spike', kocka => $tom);
$spike->pronasleduj();
$spike->set_kocka(undef);
$spike->pronasleduj();
Moose
Tento modul je postaven na meta-balíku (generátoru generátorů tříd) Class::MOP a svou syntaxí a možnostmi se spíše blíží nadcházející verzi perlu Perl6 (která je mimochodem velmi pěkná, ale o tom se přesvědčíme jindy). Je zde například podpora rolí a delegací. Navíc je možno poměrně bohatým způsobem konfigurovat accessory a vlastnosti třídy vůbec. V dokumentaci k modulu existuje řada příkladů jako Moose::Cookbook.
use strict; use warnings; package Zvire; use Moose; has 'jmeno' => (is => 'ro', required => 1);
Konstrukce atributů provádíme pomocí slova has. Atributy pak lze konfigurovat nejen pomocí direktiv Moose, ale také pomocí direktiv meta-balíku Class::MOP::Attribute.
Odvozené třídy se deklarují pomocí slova extends. S konstruktory si opět nemusíme lámat hlavu.
package Zvire::Kocka; use Moose; extends 'Zvire';
package Zvire::Pes;
use Moose;
extends 'Zvire';
has 'kocka' => (
is => 'rw',
isa => 'Zvire::Kocka',
clearer => 'clear_kocka'
);
Atribut kocka lze opět nechat automaticky typově kontrolovat. V tomto případě se můžeme spolehnout na Moose, jelikož pro vymazání atributu použijeme speciální accessor typu clearer. Accessory typu get a set se definují automaticky jako univerzální get-set accessor kocka. Pokud bychom to chtěli jinak, můžeme použít direktivy reader a writer. Lze také definovat metody pro inicializaci hodnot a predikát (test atributu na undef).
Zbytek kódu je opět téměř nepozměněn. Místo accessoru typu set s parametrem undef zde používáme přímo výše zmíněný clearer.
sub pronasleduj {
my $self = shift;
print $self->jmeno()
, defined $self->kocka()
? ' pronasleduje kocku/kocoura jmenem '
. $self->kocka()->jmeno()
: ' nepronasleduje nikoho'
, ".\n";
}
package main;
my $tom = Zvire::Kocka->new(jmeno => 'Tom');
my $spike = Zvire::Pes->new(jmeno => 'Spike', kocka => $tom);
$spike->pronasleduj();
$spike->clear_kocka();
$spike->pronasleduj();
Závěr
Představili jsme si ve zkratce tři moduly z mnoha, které více nebo méně automatizují konstrukci tříd. Má-li někdo zkušenost s jiným modulem, může přehled doplnit v diskusi. Jako vždy, před použitím některého modulu je vhodné si přečíst příslušnou dokumentaci. Pro další experimenty je možné všechny tři příklady stáhnout: Class::InsideOut , Object::InsideOut , Moose .
Na závěr malé pozorování, jak robustnost implementace ovlivňuje rychlost překladu a běhu programu.
$ time perl cio.pl user 0m0.045s $ time perl oio.pl user 0m0.140s $ time perl moose.pl user 0m0.390s