30 de septiembre de 2007

Patrón Iterador – ActionScript 3.0

Cuando tenemos colecciones de objetos a veces es necesario iterar sobre estos pero sin exponer su implementación interna, supongamos que tenemos una colección que la queremos solamente para que almacene cadenas:

1   package {
2
3 public class StringCollection {
4
5 private var _strings:Array;
6
7 public function StringCollection() {
8 _strings = new Array();
9 }
10
11 public function addElement(value:String):void {
12 _strings.push(value);
13 }
14
15 }
16 }
17


Como vemos nuestra colección tiene un método addElement que tiene la tarea de añadir cadenas al array “_strings”. Lo interesante en este caso es como podemos accesar al array “_strings” para poder iterar sobre sus valores. Una opción sería poner un método accesor en la clase, algo como esto:

1   package {
2
3 public class StringCollection {
4
5 private var _strings:Array;
6
7 public function StringCollection() {
8 _strings = new Array();
9 }
10
11 public function addElement(value:String):void {
12 _strings.push(value);
13 }
14 //Método accesor al array
15 public function get strings():Array {
16 return _strings;
17 }
18
19 }
20 }
21
22


Y el cliente utilizaría nuestra clase así:

1   var collection:StringCollection = new StringCollection();
2 collection.addElement("cadena1");
3 collection.addElement("cadena2");
4 collection.addElement("cadena3");
5 for(var i:Number = 0; i < collection.strings.length; i++) {
6 trace(collection.strings[i]);
7 }
8
9
10


Aquí hay 2 grandes problemas, estamos exponiendo los detalles de cómo debemos iterar sobre el arreglo y estamos rompiendo con el encapsulamiento al tener acceso directo al array, esto le da la oportunidad a cualquiera de poder cambiar algún valor del arreglo sin que nuestra clase se entere, que tal si en lugar de meter un string, le añado un entero ? (Recuerda que nuestra colección solamente debe almacenar cadenas).

Bueno, entonces se nos ocurre meter la responsabilidad de iterar en la misma clase, quedaría así finálmente:

1   public class StringCollection {
2
3 private var _strings:Array;
4 private var _index:Number;
5
6 public function StringCollection() {
7 _strings = new Array();
8 _index = 0;
9 }
10
11 public function addElement(value:String):void {
12 _strings.push(value);
13 }
14
15 public function reset():void {
16 _index = 0;
17 }
18
19 public function hasNext():Boolean {
20 return _index < _strings.length;
21 }
22
23 public function next():String {
24 return _strings[_index++] as String;
25 }
26 }
27 }
28



Pero nuevamente vemos problemas a la vista, supongamos que estamos en un sistema concurrente y tenemos 2 hilos que están iterando al mismo tiempo sobre la misma instancia de la colección, debido a que _index guarda la posición actual en el array, sería imposible iterar sobre la misma instancia. Otro problema vendría al querer definir nuevos tipos de iteración, por ejemplo en lugar de que la colección empiece de inicia a final, esta empiece de final a inicio, tendríamos que definir esos métodos que cumplan con esa tarea en la clase.

Y principalmente estaríamos rompiendo con un principio de diseño, SRP o Single Responsability Principle, el cual nos dice que nuestra clase solamente debe tener UNA responsabilidad (Debe tener una sola razón de cambiar). Como vemos StringCollection tiene 2 responsabilidades, la de iterar sobre la colección y la parte de la gestión de la misma (añadir, remover, obtener, etc...).

Como bien dice el sabio principio, “Encapsula lo que varíe”, en este caso la implementación de cómo iterar podría cambiar en un futuro no muy lejano.

Así que vamos a separar eso y definamos nuestra interfaz que todos nuestros Iteradores van a implementar:

1   package {
2 public interface IIterator {
3 function next():Object;
4 function hasNext():Boolean;
5 function reset():void;
6 }
7 }
8
9



El método next se encargará de devolvernos el siguiente objeto en la colección, hasNext nos devolvera true si todavía hay elementos en la colección o false en caso contrario, reset reiniciaría la posición del cursor en la colección.

Ahora vamos a hacer 2 iteradores que la implementen, cada uno con sus implementaciones concretas del CÓMO iterar.

Este es un iterador que recorre un Array de inicio a final:

1   package {
2
3 public class ArrayIterator implements IIterator {
4
5 private var _index:Number = 0;
6 private var _collection:Array;
7
8 public function ArrayIterator(collection:Array) {
9 _collection = collection;
10 _index = 0;
11 }
12
13 public function hasNext():Boolean {
14 return _index < _collection.length;
15 }
16
17 public function next():Object {
18 return _collection[_index++];
19 }
20
21 public function reset():void {
22 _index = 0;
23 }
24
25 }
26 }
27



Este es un iterador que recorre un Array de final a inicio ósea en reversa:

1   package {
2
3 public class ArrayReverseIterator implements IIterator {
4
5 private var _index:Number = 0;
6 private var _collection:Array;
7
8 public function ArrayIterator(collection:Array) {
9 _collection = collection;
10 _index = _collection.length - 1;
11 }
12
13 public function hasNext():Boolean {
14 return _index > = 0;
15 }
16
17 public function next():Object {
18 return _collection[_index--];
19 }
20
21 public function reset():void {
22 _index = _collection.length - 1;
23 }
24
25 }
26 }
27



Por último definamos la interfaz común para nuestras colecciones, esta solo consta de un método que nos devolverá el tipo de iterador deseado.

1   package {
2 public interface ICollection {
3 function iterator(type:String = null):IIterator;
4 }
5 }
6
7



Y la implementamos a nuestra existente colección StringColleciton:

1   package {
2
3 public class StringCollection implements ICollection {
4
5 private var _strings:Array;
6
7 public function StringCollection() {
8 _strings = new Array();
9 }
10
11 public function addElement(value:String):void {
12 _strings.push(value);
13 }
14
15 public function iterator(type:String = null):IIterator {
16 if(type == "ArrayReverseIterator") {
17 return new ArrayReverseIterator(_strings);
18 }else {
19 new ArrayIterator(_strings);
20 }
21 }
22 }
23 }
24



Genial !!!, ahora nuestro cliente solo sabe que nuestra clase solamente devuelve objetos que implementan la interfaz IIterator, ya no estamos exponiendo al cliente los detalles de implementación de la clase, él ya sabe que le devuelve un IIterator no importando si es un array, dictionary, etc…, sabrá que cuenta con una interfaz común para navegar sobre la colección, y en un futuro podríamos agregar más tipos de iteradores para otro tipo de colecciones sin ningún problema.

Ejemplo de uso en la parte del cliente:

1   var collection:StringCollection = new StringCollection();
2 collection.addElement("uno");
3 collection.addElement("dos");
4 collection.addElement("tres");
5 collection.addElement("cuatro");
6 var iterator:IIterator = collection.iterator();
7 while(iterator.hasNext()) {
8 trace(iterator.next());
9 }
10 iterator = collection.iterator("ArrayReverseIterator");
11 while(iterator.hasNext()) {
12 trace(iterator.next());
13 }
14
15



A poco no está fregón ?, la colección nos puede devolver 2 tipos de iteradores dependiendo del tipo de parámetro que le pasemos al método iterator, cualquiera de las 2 implementaciones concretas implementa la misma interfaz por tanto el cliente no tendrá que cambiar su código del cómo itera sobre los elementos si más adelante decide implementar otro tipo de iterador.

Aquí les dejo el diagrama de clases del ejemplo:



Cómo mejorarías el patrón compositive ó compuesto utilizando este patrón ?, bueno eso te toca a ti, ojala les haya quedado claro todo y cualquier comentario o sugerencia es bienvenida.

Para el próximo post veremos probablemente otro patrón, es una sorpresilla.

Un saludote !

28 de septiembre de 2007

Patrón Compuesto - ActionScript 3.0

Patrón Compuesto

Después de varios meses de inactividad en el blog xD (prometo no dejarlo abandonado de nuez), vamos a ver un tema muy interesante, el patrón compuesto implementado en ActionScript 3.0.

Este patrón tiene la finalidad de resolver la complejidad que podría resultar al hacer estructuras parecidas a la de un árbol de objetos, tanto la creación de estructuras complejas como la navegación sobre ellas se podrá hacer de forma sencilla implementándolo.

Imaginémonos el árbol del tule (por si no lo saben está en Oaxaca, es el árbol más gordo del mundo hahahahaha), pues el tule está formado por ramas y hojas, por ejemplo las ramas pueden tener otras ramas y otras hojas y así sucesivamente.

Nuestra rama es una composición de uno o más objetos similares que tiene una funcionalidad similar y también tenemos a las hojas que son objetos únicos, no tienen otros objetos dentro de ellas como las ramas.

Un ejemplo:

La clave está en implementar la misma interfaz tanto para los objetos compuestos (ramas) como las hojas (objetos únicos), todo esto hace que programemos por interfaz sin importar que tipo de objetos estemos utilizando a nuestra clase gestora de ramas y hojas (Árbol del Tule) no le importa que tipo concreto le demos, a esta solo le importa que le demos instancias de clase que implementen la interfaz.

Veamos como haríamos nuestro diagrama implementando el patrón:


Como podemos ver tanto nuestras clases Hoja y Rama implementan la interfaz ITreeElement y la Rama a su vez puede contener más instancias de clase que implemente la interfaz ITreeElement. La clase Árbol es la que va a ser utilizada por el cliente para formar la estructura del Tule (si que le va a costar trabajo), agregando ramas o hojas.

Ahora si empecemos a codificar el diagrama, empecemos por la interfaz:




1 package {
2
3 public interface ITreeElement {
4 function addElement(element:ITreeElement):void;
5 function removeElement(element:ITreeElement):Boolean;
6 function getInfo():String;
7 }
8 }



Ahora las 2 clases concretas (Rama y Hoja) que implementarán la interfaz ITreeElement:



1   package {
2
3 import mx.collections.ArrayCollection;
4
5 public class Rama implements ITreeElement {
6
7
8 private var _elementos:ArrayCollection;
9
10
11 public function Rama() {
12 _elementos = new ArrayCollection();
13 }
14
15 public function addElement(element:ITreeElement):void {
16 _elementos.addItem(element);
17 }
18
19 public function removeElement(element:ITreeElement):Boolean {
20 for(var i:Number = 0; i < _elementos.length; i++) {
21 if(_elementos.getItemAt(i) == element) {
22 _elementos.removeItemAt(i);
23 return true;
24 }
25 }
26 return false;
27 }
28
29 public function getInfo():String {
30 var output:String = "\n--------------------------------------\n";
31 output += "Soy una rama y tengo "+_elementos.length+" elemento(s) : ";
32 for(var i:Number = 0; i < _elementos.length; i++) {
33 var elemento:ITreeElement = _elementos.getItemAt(i) as ITreeElement;
34 output += elemento.getInfo();
35 }
36 output += "\n--------------------------------------\n";
37 return output;
38 }
39 }
40 }



Como vemos la clase Rama tiene una propiedad que es una colección de elementos, esta es la que contendrá otras ramas ó hojas que se le vayan agregando.

Algo muy importante es que le pongas mucha atención al método getInfo, ve como iteramos sobre todos los elementos de la rama y delegamos la tarea de obtener la información de los elementos sin importarle sin son ramas o hojas.

Si nos ponemos a pensar un poquito cuando hagamos nuestra clase Hoja, esta tendrá métodos que no tienen ningún significado para esta como addElement y removeElement, ya que las hojas no pueden contener otros elementos.


¿ Qué podemos hacer para solucionar este problema ?


Una solución es poner los métodos sin ningún tipo de implementación o podemos que los métodos que no tienen ningún sentido para la hoja arrojen una excepción, optaremos por la segunda opción.



1   package {
2
3 public class Hoja implements ITreeElement {
4
5 public function Hoja() {
6 }
7
8 function addElement(element:ITreeElement):void {
9 throw new Error("Este método no es posible llamarlo desde una instancia Hoja");
10 }
11
12 function removeElement(element:ITreeElement):Boolean {
13 throw new Error("Este método no es posible llamarlo desde una instancia Hoja");
14 }
15
16 function getInfo():String {
17 return "Soy una hoja sana y fuerte.";
18 }
19 }
20 }
21



Ve como en la hoja solamente necesitamos retornar la cadena de información con getInfo, ya que es un elemento único.

Luego aquí va nuestra clase Árbol que el cliente usará para formar su arbolote:



1   package {
2
3 import mx.collections.ArrayCollection;
4
5 public class Arbol {
6
7
8 private var _nombre:String;
9 private var _peso:String;
10 private var _diametro:String;
11 private var _elementos:ArrayCollection;
12
13
14 public function Arbol(nombre:String, peso:String, diametro:String) {
15 _nombre = nombre;
16 _peso = peso;
17 _diametro = diametro;
18 _elementos = new ArrayCollection();
19 }
20
21 public function get nombre():String {
22 return _nombre;
23 }
24
25 public function get peso():String {
26 return _peso;
27 }
28
29 public function get diametro():String {
30 return _diametro;
31 }
32
33 public function agregarElemento(e:ITreeElement):void {
34 _elementos.addItem(e);
35 }
36
37 public function obtenerEstructura():void {
38 for(var i:Number = 0; i < _elementos.length; i++) {
39 var elemento:ITreeElement = _elementos.getItemAt(i) as ITreeElement;
40 trace(elemento.getInfo());
41 }
42 }
43
44 }
45 }



Por último un ejemplo de cómo podríamos formar un árbol con ramas y hojas, y al final obtener su estructura interna.



1   var rama1:Rama = new Rama();
2 rama1.addElement(new Hoja());
3 rama1.addElement(new Hoja());
4 rama1.addElement(new Hoja());
5 var rama2:Rama = new Rama();
6 rama2.addElement(new Hoja);
7 rama1.addElement(rama2);
8 var rama3:Rama = new Rama();
9 rama3.addElement(new Rama());
10 var hojaSola:Hoja = new Hoja();
11
12 var arbol:Arbol = new Arbol("Árbol del Tule", "636 toneladas", "14.05 m");
13 arbol.agregarElemento(rama1);
14 arbol.agregarElemento(rama3);
15 arbol.agregarElemento(hojaSola);
16
17 arbol.obtenerEstructura();



Es un ejemplo que lo podemos mejorar utilizando el patrón Iterator, en mi siguiente post lo veremos en detalle.

Cualquier comentario ó sugerencia es bienvenida.

Un saludote !