Moose
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.
Sumario
Enlaces y referencias
- Ref: Presentación PDF de Ricardo Signes en OSCON 2010
- Ref: Moose en la wikipedia inglesa
- Ref: Presentación de Moose en perl.org
- Ref: Introduction to Moose by Dave Rolsky
- En catalyzed.org:
- En O'Reilly:
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.
- 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
- Pueden añadir a una clase métodos, modificadores y atributos.
- Pueden exigir a la clase que se proporcionen otros métodos para funcionar y
- 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 |
|
---|---|
Métodos globales |
|
Manipulación según índice |
|
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 |
|
---|---|
Manipulación de elementos |
|
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;