V Moose máme speciální aplikační rozhraní, díky němuž jsme schopni v jistém ohledu modifikovat samotný objektový systém.
16.5.2010 00:00 | Jiří Václavík | přečteno 11209×
Pomocí rozhraní meta API lze nastavovat základní vlastnosti objektového systému a ovlivňovat věci jako chování metod, atributů a tříd.
Základní metodou pro práci s vlastnostmi objektového systému je meta, která zpřístupňuje metadata dané třídy. Zde je příklad užití. Takto lze získat seznam metod nebo atributů s vlastnostmi.
$meta = MojeTrida->meta();
for $atribut ($meta->get_all_attributes){
print "Atribut: ", $atribut->name(), " ";
print $atribut->type_constraint->name if $atribut->has_type_constraint;
print "\n";
}
for $method ($meta->get_all_methods){
print $method->name," ";
}
Poznamenejme, že namísto MojeTrida na prvním řádku můžeme použít slovo __PACKAGE__, které vyjadřuje jméno aktuálního balíku.
$meta = __PACKAGE__->meta;
Podobně lze získat metodou linearized_isa seznam rodičovských tříd a pomocí subclasses podtřídy.
Pro zajímavost uveďme, že třídy lze upravovat přímo pomocí změny metadat. Ukažme si, jak lze přidávat metody a atributy. Máme zde k dispozici metody add_method a add_attribute, kterým předáme příslušné parametry.
$meta = __PACKAGE__->meta;
$meta->add_method("nova_metoda" => sub{udelej_neco(@_)});
$meta->add_attribute(
name => "novy_atribut",
is => "rw",
isa => "Int"
);
Toto chování lze vypnout pomocí metody make_immutable (případně zapnout pomocí make_mutable). V takovém případě bude při pokusu o změnu třídy pomocí metadat vyvolána výjimka.
$meta->make_immutable;
Atribut v Moose je ve skutečnosti objekt typu Moose::Meta::Attribute. To zásadně mění náš pohled na ně.
K těmto objektům lze samozřejmě přistupovat. K tomu slouží metoda get_attribute("jméno atributu"). Například objekt reprezentující atribut nazev získáme následovně.
$nazev = $meta->get_attribute("nazev")
Tento objekt má k dispozici několik metod. Pro příklad uvedeme metodu, která vrátí název datového typu.
print $nazev->type_constraint;
Abychom lépe nahlédli na strukturu, uvědomme si, že místo předchozích volání lze typ získat jedním příkazem.
print MojeTrida->meta->get_attribute("nazev")->type_constraint;
Celá situace je o to zajímavější, že strukturu třídy reprezentující atribut lze měnit. Můžeme například přidávat atributy k atributům. Jinými slovy chceme převzít řízení toho, jaký vliv na chování bude mít hash předávaný při vytváření atributu.
Ukážeme si, jak k atributu přidat popisek. Tedy chceme v řídícím hashi vytvořit nový klíč popisek. Názorněji, požadujeme, aby fungoval následující kód.
package MojeTrida::Main;
use Moose;
has "atribut_s_popiskem" => (
is => "rw",
isa => "Str",
popisek => "Toto je meta-popisek atributu atribut_s_popiskem",
);
Je dobré si zde uvědomit, že zde opravdu má smysl chtít vytvářet atributy. Popisek je totiž vlastností našeho atributu a proto by nemělo smysl ho uvádět kamkoliv jinam (například do metod naší třídy).
Této funkcionality se dosáhne tak, že definujeme vlastní metatřídu. Každý atribut je objekt typu Moose::Meta::Attribute. My ji potřebujeme přetížit. To znamená, že vytvoříme třídu, kterou nazveme MojeTrida::Meta::Attribute::Popisek, která bude dědit od Moose::Meta::Attribute. V této nové třídě definujeme nový atribut. Zároveň bude třeba někde uvést, že se daný atribut má řídit naší třídou MojeTrida::Meta::Attribute::Popisek (nikoliv standardní třídou Moose::Meta::Attribute).
Poslední požadavek (nastavení metatřídy) se vyřeší přidáním klíče metaclass. Definici našeho atributu tedy lehce upravíme tím, že přidáme jeden řádek.
has "atribut_s_popiskem" => (
metaclass => "MojeTrida::Meta::Attribute::Popisek",
is => "rw",
isa => "Str",
popisek => "Toto je meta-popisek atributu atribut_s_popiskem",
);
Nyní již Moose ví, že chceme přetěžovat. Zbývá tedy pouze vytvořit třídu MojeTrida::Meta::Attribute::Popisek, která zdědí vše od Moose::Meta::Attribute. Vložíme sem navíc nový atribut popisek. Pomocí predicate=>je_popisek navíc přidáme detektor, protože bude časem potřeba.
package MojeTrida::Meta::Attribute::Popisek;
use Moose;
extends "Moose::Meta::Attribute";
has "popisek" => (
is => "rw",
isa => "Str",
predicate => "je_popisek"
);
Nyní můžeme standardně pracovat s objekty typu MojeTrida::Main. Podívejme se pro zajímavost, jak získáme text popisku.
package main;
print MojeTrida::Main->meta->get_attribute("atribut_s_popiskem")->popisek;
Lze sem samozřejmě přistupovat i pomocí objektu.
$a = MojeTrida::Main->new(atribut_s_popiskem=>"hodnota");
print $a->meta->get_attribute("atribut_s_popiskem")->popisek;
Pro přehlednost napíšeme v MojeTrida::Main ještě metodu metainfo, která nám vytiskne informace o všech atributech a jejich popiscích. Odlišíme tak hodnotu atributu a popisek. Metoda metainfo může vypadat takto.
sub metainfo {
my $self = shift;
my $info = "";
for my $a ($self->meta->get_attribute_list){
my $atribut = $self->meta->get_attribute($a);
$info .= $atribut->name. ":\n";
$info .= " popisek: ".$atribut->popisek."\n"
if $atribut->isa("MojeTrida::Meta::Attribute::Popisek") and $atribut->je_popisek;
$info .= " hodnota: ". $self->{$atribut->get_read_method}. "\n";
}
return $info;
}
V našem příkladu bychom po zavolání print $a->metainfo; obdrželi následující výstup.
atribut_s_popiskem:
popisek: Toto je meta-popisek atributu atribut_s_popiskem
hodnota: hodnota
Budeme-li chtít pro každý atribut vytvořit více různých atributů, pak se stává naznačený postup nečitelný - lze totiž sice podobně jako pro popisek vytvářet další a další podtřídy, avšak po jisté době se každý zamyslí, zda-li je kombinování nesourodých tříd opravdu nezbytné.
Řešení nabízejí role. Klíčové slovo has totiž přijímá jako parametr mimo jiné i traits=>[qw(role1 role2 ...)]. Trait je speciální role, která se použije na objekt a od běžných rolí se jinak ničím neliší.
Modifikujme tedy předchozí příklad tak, aby dělal to samé, ale využil přitom rolí.
Nejprve definujme parametry pro atribut_s_popiskem. Zde vložíme klíč traits, do kterého nyní zadáme pouze jednoprvkový seznam (ačkoliv bychom zde mohli použít více rolí).
has "atribut_s_popiskem" => (
traits => ["MojeTrida::Meta::Attribute::Trait::Popisek"],
is => "rw",
isa => "Str",
popisek => "Toto je meta-popisek atributu atribut_s_popiskem",
);
Podtřídu MojeTrida::Meta::Attribute::Popisek nahradíme naši novou rolí MojeTrida::Meta::Attribute::Trait::Popisek. Celkem bude vypadat následovně.
package MojeTrida::Meta::Attribute::Trait::Popisek;
use Moose::Role;
has "popisek" => (
is => "rw",
isa => "Str",
predicate => "je_popisek"
);
V metodě metainfo pouze zaměníme volání metody isa na volání does. To proto, že metoda isa je vztah pro podtřídu, avšak nyní máme roli a tak použijeme does.
Vše ostatní může zůstat stejné. Nyní tedy pro přidání dalších atributů stačí do traits přidat MojeTrida::Meta::Attribute::Trait::Cokoliv a tuto roli implementovat podobně jako jsme to dělali u popisku.
Jednou z aplikací meta API je též tzv. MooseX. Díky MooseX si každý může napsat vlastní Moose rozšíření. Řada rozšíření je již k dispozici na CPAN. Návod a možnosti pro rozšiřování je v dokumentaci.