Telephone +44(0)1524 64544
Email: info@shadowcat.co.uk

El Moose està volant (2a part) (juliol 07)

Thu Nov 18 15:30:35 2010

Digg! submit to reddit Delicious RSS Feed Readers

Columna 95 del Linux Magazine (juliol de 2007)

[títol suggerit: «El Moose està volant (2a part)»]

El mes passat, vaig introduir el sistema d'objectes Moose recorrent el codi desenvolupat a la pàgina de manual de perlboot , reescrivint-ho per utilitzar Moose. Continuant a partir d'aquella discussió, fem un cop d'ull a algunes de les característiques que no vaig cobrir el mes passat.

El nostre rol Animal incloïa atributs de name i color, i mètodes de speak i eat. Podem afegir un atribut d'aniversari a la barreja amb:

  has 'born' => (is => 'ro');

Com que no volem que es pugui canviar la data d'aniversari, farem que sigui no modificable. Per defecte, aquest atribut accepta qualsevol escalar, així que aquests són tots equivalentment vàlids:

  my $horse = Horse->new(born => 'yesterday', name => 'Newbie');
  my $cow = Cow->new(born => 'spring of 82', name => 'Bessie');
  my $mouse = Mouse->new(name => 'Minnie', born => '3/14/1929');
  my $racehorse = RaceHorse->new(name => 'Slew', born => [3, 5, 59]);

Podem aconseguir una mica d'ajuda de Moose per reduir el tipus permès d'aquest nou atribut aniversari utilitzant el sistema de tipus de Moose:

  require DateTime;
  has 'born' => (is => 'ro', isa => 'DateTime');

El paràmetre isa declara aquí que el paràmetre born ha de ser un objecte DateTime, o com a mínim algun tipus que respongui cert a UNIVERSAL::isa($thing, "DateTime").

Ara, si intentem assignar qualsevol cosa a l'atribut born que no sigui un DateTime, obtenim un error en temps d'execució. Així doncs, això falla:

  my $horse = Horse->new(born => 'yesterday', name => 'Newbie');

Però això funciona:

  my $horse = Horse->new(born => DateTime->now, name => 'Newbie');

La cadena DateTime per isa es refereix aquí a la classe de Perl. Tanmateix, també podem definir això com un tipus de Moose artificial:

  use Moose::Util::TypeConstraints;
  require DateTime;
  subtype 'DateTime'
    => as 'Object'
    => where { $_->isa('DateTime') };

Això funciona com abans, però ara identifica DateTime com un tipus de Moose. El tipus es crea començant amb qualsevol Object i després requering que aquest objecte passi la qualificació adicional de ser una subclasse de DateTime de Perl.

En aquest punt, podem continuar utilitzant aquest DateTime de Moose com ho fèiem abans. Una vegada hem fet això, podem derivar un subtipus del tipus. Per exemple, podem requerir que la data sigui històrica (prèvia al present):

  subtype 'HistoricalDateTime'
    => as 'DateTime'
    => where { $_ <= DateTime->now };
  has 'born' => (is => 'ro', isa => 'HistoricalDateTime');

Ara no funcionarà qualsevol DateTime. Ha de ser algun moment que no sigui al futur. L'expressió al where pot ser qualsevol expressió que retorni un valor cert/fals, utilitzant $_ com a proxy per a l'objecte en qüestió.

Seria més senzill encara si poguèssim utilitzar expressions informals com yesterday i 3/14/1929. Date::Manip enten aquestes dues. Podem analitzar les cadenes amb Date::Manip, extreure els valors dels components i després entregar-los a DateTime, però afortunadament, ja hi ha un mòdul que fa això: DateTime::Format::DateManip.

  use DateTime::Format::DateManip;
  my $yesterday = DateTime::Format::DateManip->parse_datetime('yesterday');
  my $newbie = Horse->new(born => $yesterday, name => 'Newbie');

No està malament. El nostre aprenent cavall va nèixer ahir, tal com esperàvem. Però estaria bé simplement posar yesterday al paràmetre i deixar que ho faci tot per nosaltres. I amb coercions, podem fer-ho.

Com que passar una cadena com a aniversari no està permés, podem indicar a Moose que agafi la cadena i la passi per DateTime::Format::DateManip automàticament:

  coerce 'HistoricalDateTime'
    => from 'Str'
    => via {
      require DateTime::Format::DateManip;
      DateTime::Format::DateManip->parse_datetime($_);
    };

El bloc via agafa $_ com un valor d'entrada, que s'espera que sigui una cadena (Str). L'última expressió avaluada al codi és el nou valor HistoricalDateTime. Després permetem l'ús d'aquesta coacció afegint coerce a la declaració de l'atribut:

  has 'born' => (is => 'ro',
                 isa => 'HistoricalDateTime',
                 coerce => 1,
                );

Ara el paràmetre born accepta o bé un DateTime explícit com abans, o bé una cadena simple. La cadena ha de ser acceptable per Date::Manip, que serà utilitzat per convertir la cadena a un objecte DateTime quan el nostre objecte sigui creat.

  my $newbie = Horse->new(born => 'yesterday', name => 'Newbie');
  my $mouse = Mouse->new(name => 'Minnie', born => '3/14/1929');

A més, com que la coacció de tipus està a lloc, la verificació per assegurar que el resultat és una data històrica encara està activa.

A més dels noms de les classes i Str, Moose::Util::TypeConstraints també estableix els tipus de coses com Bool i HashRef.

Fins i tot podríem tenir múltiples coaccions definides, sempre i quan siguin diferents. Per exemple, podem utilitzar una referència a una taula de dispersió al paràmetre aniversari per indicar que estem passant parells clau/valor perquè siguin entregats directament a un constructor DateTime:

  coerce 'DateTime'
    => from 'HashRef'
    => via { DateTime->new(%$_) };

I ara podem definir una referència a una taula de dispersió per definir l'aniversari:

  my $mouse = Mouse->new(
    name => 'Minnie',
    born => { month => 3, day => 14, year => 1929 },
  );

Si el valor per a born és un DateTime, s'utilitza directament. Si és una cadena, es passa a DateTime::Format::DateManip. I si és una referència a una taula de dispersió, es passa directament com una llista aplanada al constructor DateTime. Genial.

El valor que especifiquem per a default està subjecte a les mateixes coaccions i comprovacions de tipus. Podem actualitzar born a:

  has 'born' => (is => 'ro',
                 isa => 'HistoricalDateTime',
                 coerce => 1,
                 default => 'yesterday',
                );

I ara, el valor per defecte dels animals a ``nascut ahir''. Observeu que el valor per defecte encara està subjecte a les restriccions de tipus, per tant si substituïm yesterday per tomorrow, el valor per defecte serà rebutjat. Observeu que la coacció passa a mesura que cada objecte es crea, per tant el valor per defecte one minute ago ens donarà una data diferent cada vegada que sigui cridat.

Mentre estudiem què podem fer als atributs, un altre element interessant és lazy. Si el valor per defecte és car de calcular, podem dir ``no facis això fins que ho necessitis''. Per exemple, convertir yesterday a un objecte DateTime és una mica car, així que ho podem marcar com lazy:

  has 'born' => (is => 'ro',
                 isa => 'HistoricalDateTime',
                 coerce => 1,
                 default => 'yesterday',
                 lazy => 1,
                );

I parlant de suport fantàstic, mentre escrivia aquest últim pàrraf vaig descobrir un error de programació, i commentar-ho amb Stevan a l'IRC ho va solucionar abans que jo entregués l'article. Visca.

Qualsevol cosa construïda amb Moose té disponible un grau molt alt d'introspecció. Per exemple, podem demanar a un dels nostres amics animals que ens doni l'objecte meta, amb el què podrem fer més peticions:

  my $horse = Horse->new;
  my $meta = $horse->meta; # or equivalently, Horse->meta

$meta is a Moose::Meta::Class. Podem demanar al cavall per els roles:

  my @roles = @{$meta->roles};

En aquest cas, veiem que hem obtingut un rol (de tipus Moose::Meta::Role), i aconseguit el nom amb:

  map { $_->name } @roles; # qw(Animal)

cosa que ens dona un Animal tal com esperàvem. Podem demanar a l'objecte meta tots els mètodes aplicables:

  my @methods = $meta->get_method_list;

cosa que retorna

  BEGIN
  born
  color
  default_color
  eat
  meta
  name
  private_set_color
  sound
  speak

Genial. No estic segur de què fa BEGIN aquí, però la resta són elements que hem definit. Molts d'aquests mètodes es relacionen amb atributs, però podem demanar aquests definitivament utilitzant compute_all_applicable_attributes:

  my @attrs = $meta->compute_all_applicable_attributes;

El resultat és un conjunt d'objectes Moose::Meta::Attribute. Podem utilitzar el «map« sobre name com abans per aconseguir els noms:

  map { $_->name } @attrs; # qw(born color name)

També podem veure si tenen «setters»:

  grep { $_->has_writer } @attrs; # qw(color)

Observeu que només color té un «setter»: els altres dos no són modificables, així que això té sentit.

Tal com s'ha comentat abans, Moose encara està sent desenvolupat de forma activa, però és production ready mentre us mantingueu en les coses que funcionen. Trobareu l'últim Moose al CPAN, juntament amb algun altre complement base, normalment a l'espai de noms MooseX.

Per exemple, MooseX::Getopt us permet definir el vostre procés de @ARGV utilitzant coacció de tipus, restriccions de tipus, i totes aquestes coses. No he tingut temps per jugar-hi encara, però està a la meva llista de coses pendents, així que potser en parlaré a una columna futura.

De forma similar, MooseX::Object::Pluggable simplifica l'escriptura de classes que són pluggable, que significa que poden treballar bé amb complements que defineixen mètodes i atributs adicionals. (Penseu en un servidor web genèric o un «bot« IRC que té comportaments adicionals seleccionables individualment.) De nou, m'estic adonant ara d'aquests, i sembla que valgui la pena fer-ne una descripció especial més tard.

Observeu a més que el mateix Moose està construit sobre Class::MOP, que és un entorn de treball per crear entorns de treball de classes. Potser altres projectes a més de Moose també utilitzaran Class::MOP com a punt de partida. Per exemple, l'infraestructura de Class::Prototyped (que utilitzo al meu CGI::Prototype) podria construir-se sobre Class::MOP, proporcionant més flexibilitat i robustesa.

Espero que hagueu gaudit d'aquesta introducció en dues parts a Moose. Divertiu-vos jugant amb un sistema de construcció d'objectes flexible preparat per a producció. Fins aleshores, gaudiu!