11 de octubre de 2007

Patrón Decorador - ActionScript 3.0

Supongamos que estamos diseñando un juego de rol, y nuestros personajes pueden tener espadas, estas pueden ser fusionadas con gemas de varios tipos para mejorarlas, por ejemplo para darle más fuerza de ataque o más velocidad, bueno nosotros muy contentos hacemos nuestra interfaz para nuestras espadas, quedaría codificada de la siguiente manera:


1 package {
2
3 public interface ISword {
4 function render():String;
5 }
6 }


Es una interfaz muy sencilla, el método render se encarga de construir la espada y mostrarla en la salida de pantalla. Al utilizar una interfaz en un clase se está firmando un contrato de implementación, esto quiere decir que es obligatorio implementar (valga la redundancia) los métodos que se encuentran en la firma en este caso "render".

Ahora vamos a crear nuestras espadas (implementaciones concretas), por el momento vamos a hacer 2 que puedan usar los personajes del juego de rol.

La primera la llamaremos Sword of Azure:

1 package {
2 public class SwordOfAzure implements ISword {
3
4 public function render():String {
5 return "Rendering SwordOfAzure SWORD.";
6 }
7 }
8 }


Y la segunda se llamará Sword of Kyrlin:


1 package {
2 public class SwordOfKyrlin implements ISword {
3
4 public function render():String {
5 return "Rendering SwordOfKyrlin SWORD.";
6 }
7 }
8 }


Felices y contentos ya tenemos nuestras 2 super espadotas para nuestros personajes, pero ahora se nos ha ocurrido un gran idea, porque no fusionamos las espadas con gemas para que nos den puntos de ataque, agilidad o otras cosillas interesantes, por ejemplo, fusionarla con una gema roja de ataque para que le de +4 puntos de ataque ó una gema azul para que le de +4 puntos de agilidad, o hasta se puede dar la situación que se fusionen las 2 gemas para que le de +4 de ataque y +4 de agilidad.

Nosotros pensamos, mmm... vamos a crear nuestras espadas modificadas heredando de SwordOfAzure y SwordOfKyrlin, así que nos ponemos a hacer cada una de las combinaciones posibles.


1 class SwordOfAzureRedGem extends SwordOfAzure {
2 }
3
4 class SwordOfAzureBlueGem extends SwordOfAzure {
5 }
6
7 class SwordOfAzureBlueGemAndRedGem extends SwordOfAzure {
8 }
9
10 class SwordOfKrylinRedGem extends SwordOfKrylin {
11 }
12
13 class SwordOfKrylinBlueGem extends SwordOfKrylin {
14 }
15
16 class SwordOfKrylinBlueGemAndRedGem extends SwordOfKrylin {
17 }


Dejemos los detalles de implementación a un lado.

Aquí vemos que empieza a surgir un problema, el número de clases se ira incrementando de manera excesiva cuando vayamos agregando más espadas y más estilos de gemas a nuestro juego, serían demasiadas clases !!

Aquí es donde entra el patrón decorador al rescate, este nos deja extender la funcionalidad de un objeto en tiempo de ejecución de esta manera estaríamos extendiendo la funcionalidad de nuestras espadas sin recurrir a la herencia, este patrón se basa simplemente en la composición de objetos, ahora verás el porqué.

Veamos el diagrama de cómo está estructurado el patrón decorador:



Como vemos en el diagrama, “Component” puede ser una clase abstracta o una interfaz, en este caso es nuestra interfaz ISword y los “ConcreteComponent”, son nuestras espadas que implementan las interfaz ISword.

Como dice el diagrama “Decorator” debe ser una clase abstracta que implemente la interfaz “Component” y que esta encapsule la referencia de cualquier objeto que sea un “Component”, ósea que nuestro Decorador debe implementar la interfaz ISword y esta debe de tener una propiedad que apunte a un objeto del tipo ISword.



1 package {
2 /**
3 * ABSTRACT CLASS
4 */
5 public class SwordDecorator implements ISword {
6
7
8 protected var _decorated:ISword;
9
10
11 public function SwordDecorator(decorated:ISword) {
12 _decorated = decorated;
13 }
14
15 /**
16 * Abstract Method
17 * Debe ser implementado en las subclases
18 */
19 public function render():String {
20 return "";
21 }
22 }
23 }


El método render debe ser abstracto para que sea implementato en los decoradores, como AS3.0 todavía no soporta ni las clases abstractas ni los métodos abstractos, le he puesto un comentario y una implementación por defecto, pero ese método deber ser sobreescrito en sus decoradores (subclases).

Así que vamos a crear nuestros primer decorador:



1 package {
2 /**
3 * Esta Gema nos da +4 puntos de agilidad a nuestra espada.
4 */
5 public class BlueGem extends SwordDecorator {
6
7 public function BlueGem(decorated:ISword) {
8 super(decorated);
9 }
10
11 override public function render():String {
12 calculateSomethingImportant();
13 return _decorated.render() + " , +4 puntos de agilidad";
14 }
15
16 /**
17 * Método que hace un cáculo
18 */
19 private function calculateSomethingImportant():void {
20 //...
21 }
22 }
23 }


Y este es nuestro segundo decorador:


1 package {
2 /**
3 * Esta Gema nos da +4 puntos de ataque a nuestra espada.
4 */
5 public class RedGem extends SwordDecorator {
6
7 public function RedGem(decorated:ISword) {
8 super(decorated);
9 }
10
11 override public function render():String {
12 return _decorated.render() + " , +4 puntos de ataque";
13 }
14 }
15 }


Ve como en las 2 clases he heredado de la clase Abstracta SwordDecorator, y en el constructor le paso la referencia de un objeto del tipo ISword, aquí una cosa muy importante es ver como en cada clase concreta se implementó el método render ya que es un método abstracto de la super clase y observa como llamo al método render del objeto que se paso como argumento en el constructor (_decorated) y de acuerdo a lo que me devuelva, hago una implementación nueva, en este caso le añadí una nueva cadena a la cadena que nos devuelve el objeto decorado.

El código de la parte del cliente se vería así:


1 var mySword:ISword = new SwordOfKyrlin();
2 mySword = new RedGem(mySword);
3 mySword = new BlueGem(mySword);
4 trace(mySword.render());


Como vemos a nuestra espada la hemos fusionado con 2 gemas y le hemos añadido funcionalidades diferentes, si comparamos el número de clases de la primera solución con esta última, es una GRAN diferencia, nosotros podemos agregar más decoradores y espadas sin mucho problema.

Cualquier duda, comentario o sugerencia es bienvenida.

Un Saludote !