Abril 06, 2005

Un ejemplo del patrón Extension Objects

¿Recuerdas al Professor Dispar?. ¿Recuerdas sus malvados planes para dominar el mundo?.

Hoy veremos cómo el patrón Extension Objects ( o "cómo cambiar el interfaz que implementa una clase en tiempo de ejecución" ) ha ayudado al Profesor Dispar. Pero no va a ser una tarea fácil, porque este patrón es muy complejo, pero ¿quién dijo que ser un genio del mal fuera fácil?.


En el anterior episodio, el Profesor Dispar diseñó una máquina para clonar que era capaz de clonar cualquier animal, sin conocer su raza, credo o religión, delegando los detalles del proceso de clonación en el propio animal.

Pues aquí empieza el episodio de hoy. El Profesor Dispar ha clonado ya nueve diez mil ovejas y nueve mil vacas ( dijo algo así como "ése el número exacto que necesito, juajuajuajajajajajaaaaaaa" ). Pero de repento, se dió cuenta que sí, va a necesitar muchas ovejas, y sí, va a necesitar muchas vacas, pero no todas las ovejas ( ni todas las vacas ) deberán tener el mismo rol. ¿Por qué?. Bueno, dominar el mundo no es fácil. Hace falta organización. ¡Hace falta un ( malvado ) plan!.

Y el Profesor Dispar tiene un plan. Quiere entrenar a algunas de sus ovejas y de sus vacas como soldados. Quiere que sean máquinas de matar inmisericordes ( perdón por la violencia, pero es lo que tiene ser un genio loco ). Pero también quiere que algunas de las ovejas y algunas de las vacas trabajen en las fábricas haciendo armas. Y también necesitan que algunas ovejas y algunas vacas trabajen como granjeras, cultivando los alimentos de sus compañeras militares ( recuerda, el Profesor Dispar está loco, pero no es idiota ). Por tanto, se puede decir que lo que necesita es poder asignar distintos roles a los clones que ha creado.

soldierSheep.jpg
Una oveja soldado. Sin comentarios.

[Nota]. Con el fin de mantener este tinglado un poco organizado, con un único patrón por post, escribiré las clases Sheep y Cow desde el principio. Pero el Profesor Dispar no lo hará así. Él añadirá el código del episodio de hoy al que ya tenía.

Bien, pues el genio del mal empieza a pensar. "Muy bien, tengo una clase que representa una oveja ( Sheep ). Y ahora necesito tener una oveja que se comporte como un soldado, y otra que se comporte como una campesina, y lo mismo con las vacas". Eso suena a "extender la funcionalidad de una clase", ¿no?. ¡Brillante!! "Escribiré una clase SoldierSheep ( oveja soldado ) que extienda a Sheep, y otra clase, PeasantSheep ( oveja campesina ), que también extienda a Sheep". ( Insértense cinco minutos de risas histéricas en este punto, por favor ).

Pero antes de ponerse a escribir código, el Profesor Dispar se da cuenta que probablemente ésa no sea la mejor solución, porque cada vez que quiera asignar un nuevo rol a una oveja, tendrá que escribir una nueva subclase ( por ejemplo, una OvejaIngeniera, o una OvejaMaitre... ). Y además, él no sabe a priori cuántos roles va a necesitar asignar a una oveja. Todo depende de lo que necesite en el futuro. Necesita dar soporte a la adición de interfaces desconocidos a la clase Sheep ( ¿no es un genio? )

También se da cuenta que su primera forma de atacar el problema tiene otro punto débil, y es que no podrá re-asignar un rol. Si crea una OvejaSoldado, tendrá una OvejaSoldado para siempre, aunque la necesite para cultivar cebollas ( recuerda, el Profesor está loco, a veces tiene unas ideas muy extrañas )

Pero también se da cuenta de otro punto débil, éste mucho más sutil. Una OvejaSoldado y una Oveja son exactamente lo mismo. La única diferencia es que una de ellas tiene un comportamiento particular, pero en esencia, son lo mismo. Por tanto, no es muy apropiado representarlas utilizando clases distintas.

El Profesor Dispar está loco, pero no es idiota ( sí, ya sé, ya sé que ya lo sabes ). Por tanto, decide que ha encontrado demasiados puntos débiles para su idea inicial, y que por tanto, es el momento de acudir a google en busca de una solución.

peasantSheep.jpg
Una oveja campesina. Lo creas o no, su hobby es la jardinería.

Tras varias búsquedas fallidas, recuerda un libro (Pattern Languages of Program Design, Volume 3, Addison-Wesley, 1998. ), en el que Erich Gamma ( sí, uno de los autores del GoF ) escribió un capítulo sobre el patrón Extension Objects.

Este patrón intenta anticipar la extensión del interfaz de un objeto en el futuro ( eso quiere decir: se pueden añadir interfaces a una clase en tiempo de ejecución ).

Así que empieza a leer, y a reirse. Y cuanto más lee, más se ríe.

La idea es muy sencilla. Una clase ( Sheep ) será capaz de cambiar el interfaz que implementa en tiempo de ejecución, seleccionando ese interfaz de entre una colección de objetos ( los Extension Objects ). Cada uno de esos objetos encapsulará uno de los roles ( SoldierSheep, PeasantSheep,... ) ( A estas alturas, el Profesor Dispar ya lleva casi veinte minutos de risas histéricas!! )

Pero ¿cómo será capaz la clase Sheep de seleccionar el interfaz a implementar? ¿ Y la clase Cow?. Bien, ambas clases implementarán el siguiente interfaz:

interface ISubject
{
 public function getExtension( extName: String ): Role;
}

La clase Sheep implementa un interfaz para encapsular las acciones que implementa por ser un animal ( comer, mover las piernas, los brazos... perdón: las patas, las patas... )

interface IBasicActions
{
 public function moveArms( );
 public function moveLegs( );
 public function eat( );
}

Por tanto, la clase Sheep será:

class Sheep implements ISubject, IBasicActions
{
 private var soldierRole: SoldierRole;
 private var peasantRole: PeasantRole;
    
 function Sheep( )
 {
  trace( "Soy una oveja" );
 }
     
 public function getExtension( extName: String ): Role
 {
  var returnValue: Role = null;
      		
  if( extName == "SoldierRole" )
  {
   if( soldierRole == null )
   {
    returnValue = new SoldierRole( this );
   }
   else
   {
    returnValue = soldierRole;
   }
  }
      	
  if( extName == "PeasantRole" )
  {
   if( peasantRole == null )
   {
    returnValue = new PeasantRole( this );
   }
   else
   {
    returnValue = peasantRole;
   }
  }
  		        
  return returnValue;
 }
     
 public function moveArms( )
 {
  // implementación del movimiento
  trace( "la oveja mueve un brazo" );
 }
 
 public function moveLegs( )
 {
  // implementación del movimiento
  trace( "la oveja mueve una pierna" );        
 }
     
 public function eat( )
 {
  // implementación del sistema de alimentación
  trace( "munch. munch" );        
 }    
}

Fíjate en cómo esa clase implementa el método getExtension, que elige la clase que debe devolver de entre una colección de roles ( que son variables de la clase ). ¿ Y los roles ?

Aquí está el rol base ( no he implementado nada en él, pero cualquier funcionalidad común a los roles debería implementarse aquí )

class Role
{
 function Role( )
 {
 }
}

Por tanto, el rol de soldado:

class SoldierRole extends Role implements ISoldierActions
{
 private var subject: IBasicActions;
     
 function SoldierRole( subject: IBasicActions )
 {
  this.subject = subject;
          
  trace( "Se crea el rol de soldado" );
 }
 
 public function destroy( )
 {
  //Comportamiento específico
  trace( "Soldier interface. Destruye!" );
          
  //Utiliza alguno de los métodos del animal
  subject.eat( );
 }
 
 public function moveTo( )
 {
  //Comportamiento específico
  trace( "Soldier Interface. moveTo" );
          
  //Utiliza alguno de los métodos del animal
  subject.moveLegs( );
 }
 
}

Y el rol de campesino:

class PeasantRole extends Role implements IPeasantActions
{
 private var subject: IBasicActions ;
 
 function PeasantRole( subject: IBasicActions )
 {
  this.subject = subject;
          
  trace( "Se crea el rol de campesino" );
 }
 
 public function driveTo( )
 {
  //Comportamiento específico
  trace( "Lo llevo " );
          
  //Utiliza alguno de los métodos del animal
  subject.moveArms( );
  subject.moveLegs( );
 }
 
 public function doGardening( )
 {
  //Comportamiento específico
  trace( "Vale, cuido el jardín" );
          
  //Utiliza alguno de los métodos del animal
          
  subject.moveArms( );
 }	
}

Ambas clases ( SoldierRole y PeasantRole ) implementan, respectivamente, los interfaces ISoldierActions e IPeasantActions. Esos son los interfaces que parecerá que implementa la clase Sheep

Mira:

var sheep: Sheep = new Sheep( );

var soldierSheep: ISoldierActions = ISoldierActions( sheep.getExtension( "SoldierRole" ) );

soldierSheep.destroy( );
        
var peasantSheep: IPeasantActions = IPeasantActions( sheep.getExtension( "PeasantRole" ) );
        
peasantSheep.doGardening( );

( Insértense treinta minutos de risas histéricas aquí ). Pero, un momento!!!. El Profesor Dispar se acaba de dar cuenta de un pequeño problema! ( bueno, en realidad no es un problemas, es una solución difícil )

La clase Sheep tiene conocimiento de sus extensiones ( los roles ). Esas extensiones se guardan en variables de clase, así que no hay forma de añadir algún rol, o modificar los ya existentes en tiempo de ejecución

Por tanto, el Profesor decide refactorizar la clase Sheep. Esta clase ya no guardará sus posibles extensiones en variables de clase, sino que manejará una colección ( un HashMap, un objeto ) con esos roles. De esta forma, será posible añadir o eliminar extensiones ( roles ) cuando se necesite ( no te creerías cómo se está riendo ahora ).

El nuevo interfaz ISubject:

interface ISubject
{
 public function getExtension( extName: String ): Role;
 public function addExtension( extName: String , extension: Role );
 public function removeExtension( extName: String );	
}

Y la nueva clase Sheep ( no me gusta un pelo la forma en la que el Profesor ha implementado la colección, utilizando un objeto, pero bueno... )

class Sheep implements ISubject, IBasicActions
{
 private var rolesCol: Object;
 
 function Sheep( )
 {
  trace( "I'm a sheep" );
          
  rolesCol = new Object( );
 }
     
 public function getExtension( extName: String ): Role
 {
  return rolesCol[ extName ];
 }
     
 public function addExtension( extName: String, extension: Role )
 {
  rolesCol[ extName ] = extension;
 }
     
 public function removeExtension( extName: String )
 {
  rolesCol[ extName ] = null;
 }
     
 public function moveArms( )
 {
  // movement implementation
  trace( "the sheep moves one arm" );
 }
 
 public function moveLegs( )
 {
  // movement implementation
  trace( "the sheep moves one leg" );        
 }
     
 public function eat( )
 {
  // implements the way sheeps eat
  trace( "munch. munch" );        
 }    
}

Y el Profesor Dispar puede hacer algo como:

var sheep: Sheep = new Sheep( );

sheep.addExtension( "SoldierRole", new SoldierRole( sheep ) );

var soldierSheep: ISoldierActions = ISoldierActions( sheep.getExtension( "SoldierRole" ) );

soldierSheep.destroy();
        
sheep.removeExtension( "SoldierRole" );
        
sheep.addExtension( "PeasantRole", new PeasantRole( sheep ) );
        
var peasantSheep: IPeasantActions =  IPeasantActions( sheep.getExtension( "PeasantRole" ) );
        
peasantSheep.doGardening( );

Por tanto, los clones creados por la máquina para clonar pueden cambiar su aspecto en tiempo de ejecución ( primero pueden ser soldados, luego campesinos, más tarde lo que al Profesor le dé la gana ). El profesor ha conseguido mantener la abstracción Oveja limpia de operaciones específicas para un cliente. Eso también lo podría haber conseguido gracias a la herencia, definiendo las operaciones para los clientes en las subclases, pero eso habría generado una jerarquí difícil de manejar

La siguiente aventura del Profesor Dispar será más fácil, lo prometo. ¡Incluso los genios del mal necesitan vacaciones!

[Nota importante]. Auto crítica: El código para recuperar el rol activo no pasaría ningún control de calidad. Debería implementarse de otra forma, el uso de una cadena de texto como clave es bastante peligroso

Escrito por Cesar Tardaguila en: Abril 6, 2005 07:10 AM | TrackBack
Comentarios