Un ejemplo del patrón Extension Objects ( la versión Java )
¿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.
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.
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:
public interface ISubject
{
public Role getExtension( String extName );
}
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... )
public interface IBasicActions
{
public void moveArms( );
public void moveLegs( );
public void eat( );
}
Por tanto, la clase Sheep será:
public class Sheep implements ISubject, IBasicActions
{
private SoldierRole soldierRole;
private PeasantRole peasantRole;
public Sheep( )
{
System.out.println( "I'm a sheep" );
}
public Role getExtension( String extName )
{
Role returnValue = null;
if( extName.equals( "SoldierRole" ) )
{
if( soldierRole == null )
{
returnValue = new SoldierRole( this );
}
else
{
returnValue = soldierRole;
}
}
if( extName.equals( "PeasantRole" ) )
{
if( peasantRole == null )
{
returnValue = new PeasantRole( this );
}
else
{
returnValue = peasantRole;
}
}
return returnValue;
}
public void moveArms( )
{
// implementación del movimiento
System.out.println( "la oveja mueve un brazo" );
}
public void moveLegs( )
{
System.out.println( "mueve una pierna" );
}
public void eat( )
{
System.out.println( "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í )
public abstract class Role
{
}
Por tanto, el rol de soldado:
So, the SoldierRole:
public class SoldierRole extends Role implements ISoldierActions
{
private IBasicActions subject;
public SoldierRole( IBasicActions subject )
{
this.subject = subject;
System.out.println( "SoliderBehaviour created" );
}
public void destroy( )
{
//Comportamiento específico
System.out.println( "Soldier interface. destroy" );
//Utiliza alguno de los métodos del animal
subject.eat( );
}
public void moveTo( )
{
//Comportamiento específico
System.out.println( "Soldier Interface. moveTo" );
//Utiliza alguno de los métodos del animal
subject.moveLegs( );
}
}
Y el rol de campesino:
public class PeasantRole extends Role implements IPeasantActions
{
private IBasicActions subject;
public PeasantRole( IBasicActions subject )
{
this.subject = subject;
System.out.println( "PeasantBehaviour creado" );
}
public void driveTo( )
{
//Comportamiento específico
System.out.println( "Ya voy" );
//Utiliza alguno de los métodos del animal
subject.moveArms( );
subject.moveLegs( );
}
public void doGardening( )
{
//Comportamiento específico
System.out.println( "OK, gardening" );
//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:
public class ProfessorCoupling
{
public static void main( String[ ] args )
{
Sheep sheep = new Sheep( );
ISoldierActions soldierSheep = ( ISoldierActions ) sheep.getExtension( "SoldierRole" );
soldierSheep.destroy( );
IPeasantActions peasantSheep = ( 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:
public interface ISubject
{
public Role getExtension( String extName );
public void addExtension( String extName, Role extension );
public void removeExtension( String extName );
}
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... )
import java.util.HashMap;
public class Sheep implements ISubject, IBasicActions
{
private HashMap rolesCol;
public Sheep( )
{
System.out.println( "I'm a sheep" );
rolesCol = new HashMap( );
}
public Role getExtension( String extName )
{
return ( Role ) rolesCol.get( extName );
}
public void addExtension( String extName, Role extension )
{
rolesCol.put( extName, extension );
}
public void removeExtension( String extName )
{
rolesCol.remove( extName );
}
public void moveArms( )
{
System.out.println( "mueve un brazo" );
}
public void moveLegs( )
{
System.out.println( "la oveja mueve una pierna" );
}
public void eat( )
{
// implementación del sistema de alimentación
System.out.println( "munch. munch" );
}
}
Y el Profesor Dispar puede hacer algo como:
public class ProfessorCoupling
{
public static void main( String[ ] args )
{
Sheep sheep = new Sheep( );
sheep.addExtension( "SoldierRole", new SoldierRole( sheep ) );
ISoldierActions soldierSheep = ( ISoldierActions ) sheep.getExtension( "SoldierRole" );
soldierSheep.destroy();
sheep.removeExtension( "SoldierRole" );
sheep.addExtension( "PeasantRole", new PeasantRole( sheep ) );
IPeasantActions peasantSheep = ( 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
Comentarios
Otia! creo que me has dado una buena idea para algo que tenía en mente.
Publicado por: Albin | Abril 6, 2005 03:56 PM
Espero que no haya sido una mala idea.... ;)
Publicado por: Cesar Tardaguila | Abril 6, 2005 04:20 PM
Perfecto... buen esfuerzo, pero tengo que compilarlo para probarlo. En todo caso felicitaciones por el modo singular en que explicas patrones, ojalá sigas contándonos más.
Saludos
Publicado por: kail | Abril 6, 2005 05:42 PM
Supongo que sería conveniente subir un zip con las clases, para facilitar el que las compiléis, etc. Intentaré hacerlo siempre a partir de ahora. En este caso concreto, dentro de unas horas lo subiré.
Y gracias por los comentarios ;)
Publicado por: Cesar Tardaguila | Abril 6, 2005 05:45 PM
Esa es la base de modelos de componentes sencillos como COM de Microsoft.
Saludos!
diego
Publicado por: diego sevilla | Abril 7, 2005 07:14 PM
Es buena Cesar, además como no me considero capaza de llevarla a cabo por mi mismo quiero contarla al mundo en cuanto sepa cómo expresarme :D
Publicado por: Albin | Abril 7, 2005 11:17 PM
Una duda:
"no me gusta un pelo la forma en la que el Profesor ha implementado la colección, utilizando un objeto, pero bueno..."
¿Por qué?
Publicado por: pkito | Abril 9, 2005 11:50 AM
Es un error, el comentario corresponde al post sobre el patrón en actionscrit ( en el que sí he implementado la colección como un objeto ), y no a este post en Java, en el que la colección está implementada con un Hashmap.
Disculpad por la errata, es lo que pasa cuando se hacen dos versiones de lo mismo, y no se pone mucho cuidado. :(
Publicado por: Cesar Tardaguila | Abril 9, 2005 03:18 PM
El profesor Dispar está loco, pero no es idiota!!
Gracias a Dios posee un EGO enorme que lo obliga a contarnos los detalles de como llevará a cabo sus malvados planes.
Desopilante y extremadamente didactico, mis más sinceras felicitaciones.
Saludos!
Publicado por: Kabuby | Abril 15, 2005 06:22 PM