Moose

De Astillas.net
Módulo Moose
Versión 1.09-2
Uso Todo tipo de programas
Propósito Herramientas para crear objetos y clases en Perl5


Estoy aprendiendo a utilizar esta herramienta incluyéndola en mis nuevos programas (aunque también retoco alguno de los antiguos) por lo que la información aquí contenida está muy fragmentada e incompleta. No siempre tengo tiempo de mantener la página al día.

Enlaces y referencias

Conceptos

Construcción de objetos

El método new() acepta los valores de los atributos por referencia o por copia.

#!/usr/bin/perl

use Modern::Perl;
use utf8;

package MyPackage;
use Moose;

has 'nombre' => ( is => 'rw', isa => 'Str' );
has 'numero' => ( is => 'rw', isa => 'Int' );

package main;

my $o1 = MyPackage->new( nombre => 'coco', numero => 3 );

say $o1->nombre;

my $o2 = MyPackage->new( { nombre => 'pelaez', numero => 15 } );

say $o2->nombre;

Atributos

En la definición de un atributo se pueden emplear muchos mecanismos de Moose que facilitan su manipulación y la definición de límites.

Triggers

Los triggers o activadores son código asociado a un evento. El evento en este caso es el cambio de valor de un atributo y el código a ejecutar se llama como un método de clase por lo que recibe una instancia de la misma, así como el valor nuevo y el valor antiguo:

has 'pathname' => (
   ...
   trigger => \&_path_changed,
);

sub _path_changed {
    my ($self, $new, $old) = @_;

}

Los activadores se distinguen por:

  • Sólo se llaman cuando el valor del atributo se modifica.
  • No son llamados si el cambio procede del valor predeterminado (default) o del constructor (builder).
  • El valor anterior sólo se pasa cuando ya existía previamente, por lo que es posible distinguir cuándo se trata de la primera asignación de cuando se está actualizando.

Métodos

Modificadores de funcionamiento

Se puede alterar el comportamiento de cualquier método de un clase empleando las funciones

  • after
  • before
  • around

que junto con el nombre de un método se activan antes, después y antes y después de modificar el valor de dicho atributo.

La designación de los métodos sobre los que actúa se realiza mediante alguna de estas vías:

  • nombre del método
  • lista de nombres (pasada por referencia)
  • expresión regular (compilada y que se aplica sobre los nombres)
package MyClass;
use Moose;

...

after [ 'actualizar_precio', 'eliminar_precio' ] => sub { ... };

Delegación

La delegación en Moose consiste en crear métodos puente que se limitan a llamar a otros métodos empleando como parámetro principal el valor de un atributo. Esto permite simplificar mucho el API de una clase puesto que reduce bastante el número de métodos a emplear y oculta las complejidades internas.

Ejemplo:

package MyPackage;
use Moose;

use URI;

has 'uri' => (
      is      => 'ro',
      isa     => 'URI',
      handles => [qw( host path )],
);

package main;

my $p = MyPackage->new;

$p->host;

Moose convierte la llamada $p->host a $p->uri->host, ahorrándonos un paso y, lo mejor, ocultándonos el mecanismo interior. Algo importante (y que siempre se me olvida) es que los métodos creados lo están al mismo nivel que el atributo de la clase y no supeditados a él.

En el ejemplo anterior la clase MyPackage tendría los siguientes métodos antes de definir la delegación

 MyPackage::new
 MyPackage::BUILD
 MyPackage::DEMOLISH
 MyPackage::uri

y estos otros tras definir el mapa

 MyPackage::new
 MyPackage::BUILD
 MyPackage::DEMOLISH
 MyPackage::uri
 MyPackage::host
 MyPackage::path
 

Existen algunas excepciones para este mecanismo:

  • No se puede sustituir un método definido localmente en la clase.
  • No se puede sustituir tampoco los métodos definidos en Moose::Object tales como BUILD y DEMOLISH.

El mapa de correspondencias se crea mediante el uso de handles y puede realizarse de varias formas según los parámetros que reciba cuando se define el atributo.

Lista de métodos

Si handles recibe una lista de nombres de métodos establece una correspondencia uno a uno de nombres con ellos.

has 'foo' => (
    ...
    handles => [ qw(method1 method3) ],
);


Mapa de métodos

Si los métodos cambian de nombre se emplea una lista indexada (hash) en el que las claves son los métodos nuevos en la clase llamante y los valores los de la clase externa.

has 'foo' => (
    ...
    handles => {
      m1 => 'method1',
      m3 => 'method3',
    }
);
Currying

Una de las ventajas de emplear este mapa directo es que se puede utilizar otra característica de Moose llamada currying que consiste en automatizar el paso de parámetros a los métodos delegados.

Ejemplo:

has 'foo' => (
   ...
   handles => {
      m9  => [ method1 => '9' ],
   }
);

En el ejemplo le indicamos que cree un nuevo método llamado 'm9' que terminará siendo una llamada a $self->foo->method( '9', ...) de manera que recibirá siempre como primer parámetro el valor 9 seguido de todos los que reciba en la llamada:

# esta llamada 
$self->m9( 'hola', 'adios' );
# se convertirá en 
$self->foo->method1( '9', 'hola', 'adios' );

Un ejemplo muy práctico se da cuando se usa un atributo nativo como la lista que proporciona un método para encontrar un elemento:

has 'nombres' => (
   traits  => [ 'Array' ],
   isa     => 'ArrayRef',
   handles => (
     empieza_por_a => [ 'first' => sub { m{^[A|a]} } ],
   ),
);

package main;

# Localiza el primer nombre que empieza por la letra A
my $letra_a = $self->empieza_por_a();

En el ejemplo la llamada final sería

$self->nombres->first( sub { m{^[A|a]} } );

Expresión regular

Al emplear una expresión regular Moose explora la clase externa (es decir, el atributo isa debe contener una clase) y obtiene una lista de nombres de métodos que importar.

has 'foo' => (
    ...
    handles => qr/^method{1|3}/,
);

Roles

Si se le indica un role Moose lo inspecciona también para extraer la lista de métodos (todos) que ofrece y crea el mapa de métodos correspondiente.

has 'foo' => (
    ...
    handles => 'MyRole',
);

Función

Para personalizar completamente el mapa de métodos se puede emplear una referencia a una función. Dicha función espera dos parámetros: el primero es el atributo meta de la clase que estamos definiendo y el segundo la metaclase en la que queremos delegar. Esta función retorna un hash con el mapa.

has 'foo' => (
    ...
    handles => \&_mypackage_map_builder,
);

Roles

Los roles son un mecanismo de Moose que permiten esquivar el problema de la herencia múltiple y que consisten en incluir código externo en una clase para que forme parte de ella casi como si se hubiese hecho una copia literal del fuente. Sin embargo es código lo que se incluye, código compilado, y no texto literal. Eso permite también resolver asuntos como la colisión en nombres de métodos definidos en varios roles que se quieren incluir en una clase.

  1. Actúan de forma muy parecida a un operador include.
    • los métodos definidos en un rol se incorporan a la clase que los usa como si se hubiesen definido en ella
    • se añade código, no trozos de texto
    • se pueden incluir múltiples roles en una clase
    • Moose combina todos los roles incluidos en un rol compuesto y detecta los problemas en tiempo de compilación
  2. Pueden añadir a una clase métodos, modificadores y atributos.
  3. Pueden exigir a la clase que se proporcionen otros métodos para funcionar y
  4. Se comprometen a proporcionar un interfaz concreto y conocido

Un rol en Moose se define empleando las directivas use y no con el módulo Moose::Role:

package Role::Comments;
use Moose::Role;

requires 'write_record';

sub comment {
    my $self = shift;

    $self->write_record( '...' );
}

no Moose::Role;

1;

Para usarlo en una clase se indica con la función with:

package MyPackage;
use Moose;

with 'Role::Coments'';

sub do_stuff {

}

Atributos nativos

Array

Relación de los métodos más habituales que empleo. Para una lista completa consultar la referencia anterior.

Manipulación como lista
push($value1, $value2, ...)
Añade una lista de elementos al final del array.
pop
Retorna el último elemento del array retirándolo del mismo.
shift
Retorna el primer elemento del array retirándolo del mismo.
unshift($value1,$value2,...)
Añade una lista de elementos al comienzo del array.
Métodos globales
elements
Retorna todos los elementos del array como un array, no como una referencia.
count
Retorna el número de elementos del array.
is_empty
Retorna verdadero si el array está vacío.
clear
Borra todos los elementos de la lista.
first( sub { m/ / } )
Retorna el primer elemento sobre el que la función proporcionada como parámetro da un resultado verdadero.
first_index( sub { m/ / } )
Igual que la anterior pero retorna el índice del elemento y no el elemento en sí.
Manipulación según índice
get($index)
Retorna un elemento por su número de índice.
set($index,$value)
Asigna un valor a un elemento del array.
delete($index)
Borra un elemento del array.
insert($index,$value)
Inserta un nuevo elemento en el array en el índice indicado.

Ejemplo de atributo de tipo array:

has '_images'       =>  (
    documentation   =>  q(Lista de imágenes recuperadas de la búsqueda),
    traits          =>  [ qw(Array) ],
    is              =>  'ro',
    isa             =>  'ArrayRef[Image]',    
    handles         =>  {
        'add_images'    =>  'push',
        'next_image'    =>  'shift',
        'images_count'  =>  'count',
    },
);

Hash

En las siguientes notas hago referencia al hash como lista ya que a pesar de no ser una traducción directa siempre las he visto como tales.

Manipulación como lista
is_empty
Retorna verdadero si la lista está vacía
clear
Elimina el contenido de la lista.
count
Retorna el número de elementos de la lista.
elements
Devuelve los pares clave/valor como una lista plana.
keys
Retorna las claves como una lista.
values
Retorna los valores (no las claves) como una lista.
kv
Retorna los pares clave/valor como una lista de referencias. El primer elemento es la clave, el segundo el valor.
Manipulación de elementos
get($key,[$key,...])
Retorna los valores correspondientes a las claves indicadas.
set($key => $value,[$key => $value, ...])
Escribe los pares clave/valor en la lista y retorna los nuevos valores.
delete($key,[$key,...])
Elimina los elementos correspondientes a los valores pasados.
exists($key)
Retorna verdadero si la clave está presente en la lista.
defined($key)
Retorna verdadero si la clave indicada contiene algún valor.

Bool

Los métodos que proporciona este atributo son:

set
Pone a uno el atributo.
unset
Pone a cero el atributo.
toggle
Cambia al valor contrario; de cero a uno y viceversa.
not
Niega lógicamente el valor contenido (equivalente a not $value).

Ejemplo:

package MyPackage;
...
has 'is_lock' => (
   traits  => [ q(Bool) ],
   is      => 'ro',
   isa     => 'Bool',
   default => sub { 0; },
   handles => {
     lock_billing   => 'set',
     unlock_billing => 'unset',
     is_unlock      => 'not',
   },
);

Counter

El tipo contador puede ser sobre cualquier tipo de número (entero o no) y provee métodos para incrementar, decrementar y poner a cero. El valor predeterminado para el tipo base es Num.

Los métodos disponibles son:

set( $value )
Inicializa el contador a cierto valor.
inc( [ $arg ] )
Incrementa el contador en uno o en el valor proporcionado.
dec( [ $arg ] )
Decrementa el contador en uno o en el valor proporcionado.
reset( )
Inicializa el contador a su valor predeterminado (default); no confundir con poner a cero porque puede no serlo.
package MyPackage;
...
has 'maximo'  => (
    traits    => [ q(Counter) ],
    is        => 'ro',
    isa       => 'Int',
    default   => sub { 0; },
    handles   => {
     sumar_uno  => 'inc',
     restar_uno => 'dec',
     a_cero     => 'reset',
    }
);

String

El tipo texto (string) permite acortar un montón de operaciones con atributos textuales que van a construirse programáticamente.

Los métodos disponibles son:

append($string)
Añade el texto al final y retorna el nuevo contenido. Idéntica a $content += $string.
prepend($string)
Inserta el texto al principio y retorna el nuevo contenido. Equivalente a $content = $string . $content
replace($pattern,$replacement)
Efectúa una sustitución tipo regexp sobre el contenido. Equivalente a $content =~ s/$pattern/$replacement/s
clear()
Asigna un texto vacío (no el valor predeterminado) al atributo.
length()
Retorna la longitud del contenido del atributo igual que el operador length.
chop()
Efectúa la operación chop sobre el contenido.
chomp()
Efectúa la operación chomp sobre el contenido.
match($pattern)
Utiliza el patrón sobre el contenido del atributo y retorna el valor coincidente si existe. Equivalente a $content =~ m{$pattern}.

Ejemplo de uso:

package MyPackage;
use Moose;
...
has 'comentarios' => (
    traits  => [ qw(String) ],
    is      => 'ro',
    isa     => 'Str',
    default => q{},
    handles => {
      agregar_texto => 'append',
      borrar_texto  => 'clear',
      buscar        => 'match',
    },
);

package main;

my $obj = MyPackage->new();

$obj->agregar_texto('Este es el fin');

# Ya que el tipo básico es Str se puede utilizar
# sin necesidad de un método especial.
say $obj->comentarios;

Recetario

Construcción de objetos

Atributos

Esta sección habla sobre las herramientas de definición de atributos de una clase Moose.

lazy_build

Este parámetro permite definir las siguientes características de un atributo:

  • Atributo con valor obligatorio
  • Asignación de valor postergada (lazy)
  • Constructor definido como _build_NOMBRE_ATRIBUTO
  • Predicado definido como has_NOMBRE_ATRIBUTO
  • Borrado definido como método clear_NOMBRE_ATRIBUTO

Ejemplo:

has 'nombre' => (
   is         => 'ro',
   isa        => 'Str',
   lazy_build => 1
);

# que se expande a 
has 'nombre' => (
   is         => 'ro',
   isa        => 'Str',
   lazy       => 1,
   required   => 1,
   builder    => '_build_nombre',
   predicate  => 'has_nombre',
   clearer    => 'clear_nombre',
);

# aunque es posible cambiar los nombres empleándolos 
# directamente con el parámetro adecuado 
has 'nombre' => (
   is         => 'ro',
   isa        => 'Str',
   lazy_build => 1,
   clearer    => 'borrar_nombre',
);

Si el nombre del atributo comienza por un subrayado se entiende que va a ser privado y los nombres de los métodos automáticos cambian en consecuencia:

has '_contador' => (
     is         => 'ro',
     isa        => 'Int',
     lazy_build => 1,
);

sub _build__contador {
    return 0;
}
sub _has_contador {
   # este método lo proporciona Moose automáticamente
   ...
};
sub _clear_contador {
   # este también 
   ...
);

BUILD

El método BUILD se utiliza después de la creación del objeto, una vez que todos sus atributos han sido inicializados por lo que se puede emplear para:

  • Verificar el estado del objeto como un todo.
  • Registrar su creación.
  • Utilizar parámetros que no han sido empleados en la inicialización de los atributos.
  • Efectuar tareas que dependan de varios atributos como la carga externa de datos o ciertas inicializaciones.

La llamada a este método incorpora -además de una referencia al objeto en sí- una referencia a un hash con los parámetros recibidos en el método new. Es factible utilizar y cambiar parámetros del objeto y no es necesario llamar a otras instancias superiores en la jerarquía de herencia de clases. Moose se encarga de llamarlos en el orden conveniente.

sub BUILD {
    my $self = shift;
    my $args = shift;

    # Do something with the new's parameters 
    if (exists $args->{'myparam'}) {
        ...
    }
    
    # Check two params
    if (defined $self->param1 and defined $self->param2) {
       # kaboom
       die " ... ";
    }

    return;
}

BUILDARGS

El método BUILDARGS es utilizado antes de crear una instancia de una clase y su propósito es efectuar cambios en los parámetros de construcción (los que recibirá new()) para acomodarlos a los parámetros que exige el objeto.

Para comodidad del programador el método puede emplearse vía una función around de tal manera que situamos nuestro método en la cadena de herencia de Moose y nos aprovechamos de otras facilidades.

around BUILDARGS => sub {
   my $orig  = shift;
   my $class = shift;

   # Si sólo recibimos un parámetro y no es una referencia 
   # lo empleamos como valor del atributo ''nombre'' (ficticio)
   if (@_ == 1 and not ref($_[0]) {
       return $class->$orig( nombre => $_[0] );
   }
   else {
       return $class->$orig( @_ );
   }
}

Usado así recibimos el método original y la clase de la cual heredamos, además de la capacidad de Moose de distinguir entre una referencia a un hash y un hash directo, por lo que los parámetros en @_ ya tienen un filtrado inicial.

Como valor de retorno se espera una referencia a un hash cuyas claves coincidan los atributos del objeto (o más bien aquellos especificados en el parámetro init_arg).

Roles

Comprobar el rol de un objeto

Para comprobar si un objeto tiene un rol determinado existe el método Moose does() que permite interrogar al mismo nivel que el método can() o isa().

sub my_method {
   my ($self, $obj) = @_;

   if (defined $obj and $obj->does('Registrador')) {
     $obj->registra('lo que hago');
   }

}

En el ejemplo recibimos un objeto y antes de usar una de sus características registra consultamos si está definido y si hace el rol de Registrador.

Alterando clases

Moose emplea como base el protocolo MOP por lo que es posible alterar una clase durante la ejecución para añadirle/quitarle tanto atributos como métodos y roles.

Para ello se emplea el mecanismo de introspección detallado en Moose::Manual::MOP pero se debe tener la precaución de no definir la clase como inmutable.

El siguiente ejemplo muestra como añadir métodos a una clase durante la construcción de un objeto:

package MyPackage;
use Moose;

...

sub BUILD {
    my $self = shift;
    my $args = shift;

    my $meta = $self->meta;

    $meta->add_method( 'nuevo_metodo' => sub { ... } );

    return;
}

Pero es importante tener en cuenta que el método se crea en el espacio de nombres de la clase por lo que si, por ejemplo, el nombre del nuevo método está en función de parámetros recibidos en new(), todos los objetos creados con esa clase, no importa cuándo halla sido creada, tendrán disponible el nuevo método.

Errores y advertencias

Not inlining 'new' for

Este mensaje que continúa como ... since it has method modifiers which would be lost if it were inlined y aparece al cargar una clase Moose en la que añadimos modificadores de métodos mediante el módulo Moose::Exporter, MooseX::Declare, incluyéndolos a mano o extendemos una clase que utiliza algo de lo anterior.

Para eliminar el aviso debemos hacer la clase inmutable con un parámetro especial:

package X;
use Moose;
use Moose::Exporter;
use namespace::autoclean ;
use utf8;

extends 'Throwable::Error';

Moose::Exporter->setup_import_methods( as_is => [ \&catch ] );

#
# ... el resto del módulo y finalmente 
#

no Moose;
__PACKAGE__->meta->make_immutable( inline_constructor => 0 );
1;