Mostrando entradas con la etiqueta Objetos. Mostrar todas las entradas
Mostrando entradas con la etiqueta Objetos. Mostrar todas las entradas

martes, 5 de junio de 2012

Una introducción agradable a Moose


Lo que sigue es la versión en español del artículo A gentle introduction to Moose escrita por Jay Kuri en el 2009.


Perl, desde el lanzamiento de su versión 5 en 1994, tiene las características de un lenguaje orientado a objetos. Pero estas características OO siguen siendo algo así como un "hágalo Usted mismo", con el clásico estilo de Perl, ofreciendo un mínimo de apoyo integrado en el lenguaje y el resto se deja a cargo de los demás. 

La principal ventaja de este estilo "hágalo Usted mismo" es que el lenguaje impone muy pocas restricciones sobre la manera de hacer las cosas. Por otro lado tiene la desventaja de ser algo intimidante con los novatos. Por lo bueno y lo malo, se ha vuelto familiar.

Otra ventaja es que las características tienen la libertad de evolucionar, por no estar sujetas por el lenguaje algunos programadores muy inteligentes tuvieron la libertad de explorar formas de hacer las cosas de maneras que no estaban preconcebidas. Moose es el resultado de ésta exploración y rápidamente se convirtió en el estandar defacto para la POO en Perl. Hoy vamos a explorar las bases de la creación y manipulación de objetos en Moose.

En la convencional Programación Orientada a Objetos en Perl hay tres cosas principales que necesitan hacerse. Estas son:
  1. Creación de objetos
  2. Atributos y métodos de acceso
  3. Herencia
Tomaremos un rápido exámen alrededor de estas cuestiones y las compararemos con la técnica Perl y la de Moose.


Creación de objetos
En puro Perl, la forma de crear un objeto es crear una referencia a una variable y luego "bendecirla" con una clase. Podemos hacer esto con cualquier tipo de variable, pero lo más común es usar una referencia a un hash. Esto se hace generalmente en una subrutina 'new':
package FooClass;

  sub new {
      my $class = shift;
      my $foo = {};
      bless $foo, 'FooClass';
      return $foo;
  }

Esto devuelve un objeto. Hay algo más aún. La forma Moose de hacer esto es algo más sencilla:
  package FooClass;
  use Moose;

Eso es todo. Moose crea la subrutina new() por nosotros. Si la clase tiene atributos, Moose también se encargará de gestionarlos basándose ​​en los argumentos que se pueden colocar.
Crear una instancia de una clase sigue la misma convención en un objeto de Moose como un objeto de puro Perl: simplemente llamar a new() en la clase. Moose, sin embargo, permite pasar los valores iniciales de sus atributos en la llamada. Por ejemplo:
  FooClass->new( name => 'bob' );
Este es un poco de funcionalidad útil y gratuita que nos brinda el uso de Moose.


Atributos y métodos de acceso
En Perl, nuestro objeto es (muy a menudo) un hash y sus atributos son simplemente miembros de ese hash. De nuevo a menudo, tales atributos se acceden directamente:
  $foo->{name};
Este generalmente está mal visto, ya que proporciona muy poca estructura y hace fácil declarar mal accidentalmente los atributos. $foo-> {naem} puede ser un error muy difícil de encontrar. La "mejor práctica" generalmente consiste en crear las subrutinas de acceso para las variables, lo que limita a los que utilizan el objeto para trabajar con los atributos que hay creados. (Los geeks OO se refieren a esto como "encapsulación"). El problema es que en Perl OO lo debemos crearlas nosotros mismos:
   sub name {
        my $self = shift;
        if( @_ ) {
            $self->{'name'} = $_[0];
        }
        return $self->{'name'};
    }

Se trata de una gran cantidad de código sólo para escribir los setter/getter del objeto. En puro Perl hay un módulo de CPAN nacido para hacer esto más fácil. Class::Accessor construye estos métodos por nosotros:
  package FooClass;
  use base qw(Class::Accessor);
  FooClass->mk_accessors(qw(name age));

Class::Accessor crea el método new por nosotros. Esto es claramente mejor que hacerlo a mano. Ahora es prácticamente imposible crear accidentalmente atributos o acceder a los equivocados. Hay una cosa que no hace bien. No le impide de ninguna manera poner basura en los atributos. Esto es perfectamente legal:
  $foo = FooClass->new();
  $foo->name(192);
  $foo->age('magdalena');

Moose, por el contrario, no sólo hace lo que Class::Accessor, sino que añade la comprobación de tipos (y algunas otras cosas). El mismo objeto creado con Moose:
  package FooClass;
  use Moose;
  has 'name' => (
    is => 'rw',
    isa => 'Str'
  );

  has 'age' => (
    is => 'rw',
    isa => 'Int'
  );
  1;

Lo anterior proporciona el mismo name() y age() que estamos acostumbrados. Pero si tratamos de establecer la edad como 'magdalena' ahora vamos a obtener un error. Por otro lado también tenemos la capacidad de decir esencialmente "este atributo no se puede cambiar después de crear el objeto". Entre otras cosas, el sistema de atributos y de control de tipos de Moose es increíblemente flexible y merece su propio tutorial dedicado.


Herencia
La herencia en programación orientada a objetos está diseñada para permitir la especialización de una clase. El ejemplo clásico es que se puede tener una clase Figura que puede proporcionar un cierto grado de funcionalidad, y cuando se necesita una clase Cuadrado se puede heredar de Figura y añadir sólo el código que hace particular al Cuadrado. En Perl "puro" OO podríamos hacer esto:
  package Cuadrado;
  @ISA = ("Figura");
  # el resto de la clase Cuadrado

Perl tiene la inteligencia para buscar los métodos que no encuentra en la clase Cuadrado en las clases definidas en @ISA. Funciona bien, pero no es muy intuitivo. Moose tiene herencia también, y lo hace más evidente diciendo:
  package Cuadrado;
  use Moose;
  extends 'Figura';
  # el resto de la clase Cuadrado

Bastante sencillo


Otras cosas de Moose
Moose no hace nada que no se pueda hacer con Perl OO. Moose se basa en las mismas características OO soportadas por el lenguaje Perl. Se ocupa, sin embargo, de una enorme cantidad de trabajo que de lo contrario tendríamos que hacer nosotros mismos, dándole la libertad para trabajar en su "nueva" funcionalidad en lugar de reinventar la rueda una vez más.

En este artículo he mostrado cómo hacer las cosas "normales" de Perl OO con Moose. En este punto, usted sabe cómo hacer uso de la programación orientada a objetos básica y podría comenzar a reemplazar sus enrolladas clases hechas a mano por otras construidas con Moose.

Moose, sin embargo, es más que un poco de sintaxis inteligente y algunos ahorros de tiempo. Moose tiene algunas características adicionales que lo hacen realmente increíble para trabajar con él. Sólo voy a referirme a ellas aquí, ya que requeriría un libro entero para cubrirlas adecuadamente. Algunas de las características disponibles con Moose son las siguientes: 
  • Metadatos de la clase: La posibilidad que tiene su código de examinar la estructura de sus objetos.
  • Coerción: La capacidad de convertir los valores de un tipo a otro cuando sea necesario.
  • Modificadores de métodos: La capacidad de agregar código que se ejecuta antes, después o alrededor de un método existente.
  • Roles: La capacidad de agregar funcionalidad predefinida a las clases sin usar herencia.
Hay muchas otras que vale la pena investigar también. Los roles son especialmente interesantes y proporcionan una flexibilidad que no tiene precedentes en casi cualquier lenguaje orientado a objetos. También digno de mención son las extensiones de Moose que proporcionan una funcionalidad incluso más allá de Moose estándar.

Para obtener más información sobre Moose, eche un vistazo a la documentación. Para ver más en detalle Moose vs Perl OO, eche un vistazo a la versión "sin azúcar" del manual.

También puede consultar la página de inicio de Moose. Por último, he comenzado una lista de "Trampas de Moose" en Catalyzed Wiki para rastrear algunas de las partes más difíciles de Moose que he encontrado. Siéntase libre de agregar las propias, a medida que descubre los placeres de usar Moose.




Con Moose la POO vuela


Moose es un completo sistema de objetos para Perl 5. Defines tu clase en forma declarativa. Está basado en gran parte en el sistema de objetos de Perl 6 tomando las mejores ideas de CLOS, Smalltalk y otros lenguajes.
Crearemos la clase Transporte. Un Colectivo es un medio de Transporte. Entonces en el archivo Colectivo.pm escribimos la clase Colectivo que tiene un número de línea y un color:
  package Colectivo;
  use Moose;
  has 'nombre' => (is => 'rw');
  has 'color' => (is => 'rw');
  1;
Aquí decimos que Colectivo tiene nombre y color, y que son atributos de lectura/escritura. Podemos usar la clase:

  use Colectivo;
  my $rapido = Colectivo->new(nombre => 129);
  print $rapido->nombre; # imprime 129
  $rapido->color("amarillo"); # configura el color

Notar que no se definió el método new, Moose lo hace por nosotros.
Ahora bien, Colectivo hereda de Transporte. Para expresar eso facilmente en Transporte.pm escribimos:

  package Transporte;
  use Moose;
  has 'nombre' => (is => 'rw');
  has 'color' => (is => 'rw');
  1;

Y entonces actualizamos nuestro Colectivo.pm:

  package Colectivo;
  use Moose;
  extends 'Transporte';
  1;

Notar que 'extends' reemplaza el tradicional uso de 'base' y configura el array @ISA.

En este momento, Colectivo y Transporte son identicos. Ellos pueden ser instanciados y tienen sus dos atributos. Lo que distingue a un Colectivo de otros medios de Transporte es su bocina. Nosotros lo agregamos aquí:

  package Colectivo;
  use Moose;
  extends 'Transporte';
  sub bocina { 'tuturururu' }
  1;

y luego hacer referencia a eso en el método común "tocar" de la clase Transporte:

  package Transporte;
  use Moose;
  has 'nombre' => (is => 'rw');
  has 'color' => (is => 'rw');
  sub tocar {
    my $self = shift;
    print $self->nombre, " hace ", $self->bocina, "\n";
  }
  sub bocina { confess shift, " deberia haber definido una bocina!" }
  1;

Notar el uso de "confess", si la clase derivada no tiene definido un método bocina, se quejará. Pero como Colectivo definió su bocina, nunca ejecutará Transporte::bocina(). Ahora puedo crear mi colectivo:

  my $rapido = Colectivo->new(nombre => 129);
  $rapido->tocar; # toca la bocina "tuturururu"

Hasta ahora hemos codificado cosas que sin Moose serian simples de hacer, comenzaremos a complicarnos para ver su verdadero poder. Primero, un Transporte es una clase abstracta que se usa solamente para proveer atributos y métodos comunes a una clase concreta (en este caso, la clase Colectivo). En términos de Moose, esto se describe como un rol. Un rol nunca tiene instancias, porque no es una clase completa.

Cuando convertimos la clase Transporte en un rol obtenemos algo de soporte adicional:

  package Transporte;
  use Moose::Role;
  has 'nombre' => (is => 'rw');
  has 'color' => (is => 'rw');
  sub tocar {
    my $self = shift;
    print $self->nombre, " hace ", $self->bocina, "\n";
  }
  requires 'bocina';
  1;

Notar que reemplazamos el 'confess' por un requires. Esto informa a Moose que este rol debe ser usado con una clase que provea el método 'bocina', lo cual se chequea en tiempo de compilacion. Para el rol, nosotros lo vamos a 'usar' más que 'extenderlo':

  package Colectivo;
  use Moose;
  with 'Transporte';
  sub bocina { 'tuturururu' }
  1;

Si nos faltara incluir 'bocina' obtendríamos una notificación bien temprano. Eso es bueno! En este caso Colectivo trabaja igual que antes.

Qué pasa con 'with' y 'requires'. Debido a que son definidos por Moose y Moose::Role, ellos permanecen como parte del package. Para los puristas que hay en nosotros, no nos gusta este tipo de contaminación, nosotros podemos borrarlos cuando hayamos terminado, usando el correspondiente 'no' (similar al uso de 'strict' y 'no strict'). Por ejemplo, limpiando Colectivo.pm:

  package Colectivo;
  use Moose;
  with 'Animal';
  sub bocina { 'tuturururu' }
  no Moose; # sacando los andamios
  1;

En forma similar, Transporte.pm no requiere Moose::Role al final.

Moose soporta la noción de valor por defecto. Agregaremos un color por defecto y haremos que sea la clase la responsable:

  package Transporte;
  ...
  has 'color' => (is => 'rw', default => sub { shift->default_color });
  requires 'default_color';
  ...

Si el color no es provisto, el color por defecto de la clase será consultado a través del método default_color(), y 'requires' asegura que la clase concreta provea este método. Nuestras clases derivadas de Transporte lucirían así:

  ## Avion.pm:
  package Avion;
  use Moose;
  with 'Transporte';
  sub default_color { 'blanco' }
  sub bocina { 'trom' }
  no Moose;
  1;
  ## Colectivo.pm:
  package Colectivo;
  use Moose;
  with 'Transporte';
  sub default_color { 'amarillo' }
  sub bocina { 'tuturururu' }
  no Moose;
  1;
  ## Bicicleta.pm:
  package Bicicleta;
  use Moose;
  with 'Transporte';
  sub default_color { 'roja' }
  sub bocina { 'tilin' }
  no Moose;
  1;

Ahora tenemos Bicicleta entre nuestras clases implementadas:

  use Bicicleta;
  my $ligera = Bicicleta->new(color => 'azul', nombre => 'Ligera');
  $ligera->tocar; # imprime "Ligera hace tilin"

Bueno, esto fue bastante sencillo. Ahora resolveremos otros problemillas.

La clase Ambulancia es especial, porque cuando toca su bocina los demás transportes deben apartarse del camino. En forma tradicional usaríamos una llamada a SUPER:: para llamar al comportamiento de la clase padre, pero esto no funciona con roles. Los roles no quedan en @ISA, porque son 'pegados' en el lugar en vez de 'encimados'.
Afortunadamente, Moose provee el conveniente 'after' para agregar comportamiento adicional a un método existente. Moose reemplaza el método original conservando el contexto (lista, escalar o void) así como su valor de retorno. Nuestro método 'tocar' para Ambulancia quedaría así:

  package Ambulancia;
  use Moose;
  with 'Transporte';
  sub default_color { 'blanca' }
  sub bocina { 'huuuuuu' }
  after 'tocar' => sub {
    print "Apartense!\n";
  };
  no Moose;
  1;

Esto produce una ambulancia que funciona bien:

  my $ambu = Ambulancia->new(nombre => 'Urgencia');
  $ambu->tocar;

resulta:

  Urgencia hace huuuuuu
  Apartense!

También podemos usar 'before' y 'around' para preceder al comportamiento original o controlar la llamada del comportamiento original, según sea necesario. Por ejemplo, para permitir que 'nombre' sea usado como método de acceso y aún como un método de clase que devuelve un Transporte sin nombre, podemos rodear el 'nombre' con 'around':
  package Transporte;
  ...
  has 'nombre' => (is => 'rw');
  around 'nombre' => sub {
    my $next = shift;
    my $self = shift;
    blessed $self ? $self->$next(@_) : "un $self sin nombre";
  };
El 'has' crea el comportamiento original. El 'around' intercepta la llamada a 'nombre' y toma el coderef como primer parámetro en la variable $next y el original $self como segundo parámetro. Testea $self y si es un objeto llama al original coderef con el resto de los parámetros en @_. Así se obtiene el comportamiento original (un getter o setter) para el objeto, pero si es una clase nos devuelve un string literal.

What if we never gave our animal a nombre? We'll get warnings about undefined values. We can give a default nombre just as we did a default color:
¿Y qué sucede si nunca le damos nombre a nuestro Transporte? Vamos a recibir warnings por valores indefinidos. Podemos dar un 'nombre' por defecto tal como lo hicimos con 'color':
  has 'nombre' => (
    is => 'rw',
    default => sub { 'un '. ref shift .' sin nombre ' },
   );

De nuevo, nos place que 'around' inmediatamente siga este paso.

Si no queremos que la gente cambie el color después de la creación de la instancia inicial, podemos declarar el atributo de sólo lectura:
  has 'color' => (is => 'ro', default => sub { shift->default_color });

Ahora un intento de establecer el color aborta con el mensaje 'Cannot assign a value to a read-only accessor'. Si realmente quisiera tener una manera de establecer el color de vez en cuando, podemos definir un escritor separado:

  has 'color' => (
    is => 'ro',
    writer => 'private_set_color',
    default => sub { shift->default_color },
  );

Por lo tanto, no podemos cambiar el color de una bicileta directamente:

  my $ligera = Bicicleta->new;
  my $color = $ligera->color; # método getter obtiene el color
  $ligera->color('verde'); # MUERE

Sin embargo, podemos utilizar nuestro método privado en su lugar:
  $ligera->private_set_color('verde'); # método setter privado

Mediante el uso de un nombre de largo, hacemos menos probable que accidentalmente lo llamen, a menos que intencionalmente querramos cambiar el color.

Vamos a crear una bicicleta de carreras, BicicletaCarrera, mediante el agregado de características de competición a una bicicleta. En primer lugar definimos las características de competición usando roles, claro:
  package Competidor;
  use Moose::Role;
  has $_ => (is => 'rw', default => 0)
    foreach qw(victorias posicion shows perdidas);
  no Moose::Role;
  1;

Tenga en cuenta que desde que 'has' es solo una llamada a función, podemos utilizar las estructuras tradicionales de control de Perl (en este caso, un bucle foreach). Con un poco de código, hemos añadido otros cuatro atributos.
El valor inicial de 0 significa que no tienes que escribir el código de inicialización independiente en nuestro constructor.
Ahora, podemos añadir algunos métodos de acceso:

  package Competidor;
  ...
  sub gano { my $self = shift; $self->victorias($self->victorias + 1) }
  sub aplazado { my $self = shift; $self->posicion($self->posicion + 1) }
  sub showed { my $self = shift; $self->shows($self->shows + 1) }
  sub perdio { my $self = shift; $self->perdidas($self->perdidas + 1) }

  sub tabla_posiciones {
    my $self = shift;
    join ", ", map { $self->$_ . " $_" } qw(victorias posicion shows perdidas);
  }
  ...

Cada llamada al método 'gano' incrementa el número de victorias. Esto sería más sencillo si se presume que estos objetos se implementan como hashes (que lo son por defecto), como:

  sub gano { shift->{victorias}++; }

Sin embargo, mediante el uso de la interfaz pública (una llamada de método), podríamos cambiar la posterior implementación de adentro hacia afuera del objeto usando arreglos sin romper este código. Esto es especialmente importante cuando se crea un rol genérico, que podría mezclarse con cualquier tipo de objeto.

Para crear la bicicleta de carreras, mezlcamos una Bicicleta con un Competidor:

  package BicicletaCarrera;
  use Moose;
  extends 'Bicicleta';
  with 'Competidor';
  no Moose;
  1;

Y ahora podemos montar las bicicletas!:

  use BicicletaCarrera;
  my $bici = BicicletaCarrera->new(nombre => 'ligera');
  $bici->gano; $bici->gano; $bici->gano;
  $bici->aplazado;
  $bici->perdio; # corremos algunas carreras
  print $bici->tabla_posiciones, "\n";
  # 3 ganadas, 1 aplazada, 0 shows, 1 perdida

Hasta ahora, sólo he arañado la superficie de lo que ofrece Moose. Ahora a codificar y ganar experiencia!