Octubre 17, 2005
Un ejemplo del patrón... ¡adivínalo!
EDITADO: El patrón es el Strategy. Por tanto, el título del post debería ser "Un ejemplo el patrón Strategy en actionscript"
El Profesor Dispar ha estado disfrutando de unas merecidas vacaciones tras conquistar el mundo (sí, desde la última vez que tuvimos noticias de él, ha conseguido llevar a buen puerto sus maléficos planes).
Pero el día a día de dominar el mundo le está matando de aburrimiento. El Profesor Dispar echa de menos los viejos tiempos, cuando nadie le comprendía, cuando podía odiar a todos los líderes del mundo porque le ignoraban... Ahora pasa la mayor parte del día haciendo papeleo, y añora los días en los que se podía dar una vuelta por los campamentos de sus tropas y confraternizar con ellos, contar chistes, tomar unas cervecitas...
Así que, en un arranque de genio (malvado, claro), ha decidido que, para matar el aburrimiento, quiere ver un desfile cada día. Mejor dicho, un desfile diferente cada día. Un día le pedirá a su Mariscal que le prepare un desfile con los chicos de la compañía B, otro día querrá que desfilen los soldados cuyo nombre contenga una P... ahhh, los genios del mal...
Las tropas del Profesor Dispar están bien entrenadas. Saben que, en caso de ser capturadas, sólo deben decir su nombre, compañía, y número de serie. Nada más. Así que podríamos abstraer un soldado como algo parecido a esto:
class Soldier { private var name : String; private var id : String; private var company : String; private var weapons : Object; function Soldier( initInfo: Object ) { this.name = initInfo.name; this.id = initInfo.id; this.company= initInfo.company; } public function getPublicInfo( ) { return { name: this.name, id: this.id, company: this.company }; } //Other methods public function toString( ): String { return "soldier{ name: " + this.name + ", id: " + this.id + ", company: " + this.company + " }"; } }
Lo que define, pues, a un soldado es su nombre, su compañía, y su número de serie, y esa es la única información que proporcionarán cuando se les pregunte.
Imagina que el Profesor Dispar le pide al Mariscal que prepare un desfile con los soldados de la compañía B. El Mariscal, entre cuyas responsabilidades están jugar al golf y mantener en forma a sus soldados, tendrá que ir preguntando a los soldados a qué compañía pertenecen, y seleccionará, por tanto sólo a aquellos que sirvan en la compañía B. Pero ahora, imagina que el Profesor quiere que desfilen sólo los soldados en cuyo nombre haya una letra "P". Entonces, el código del Mariscal será algo así:
class Marshal { public static var CONTAINS_LETTER: String = "Letter"; public static var COMPANY : String = "Company"; private var soldiersList: Array; function Marshal( ) { this.soldiersList = new Array( ); } public function recruitSoldier( newSoldier: Soldier ) { this.soldiersList.push( newSoldier ); } public function getFilteredCollection( filterType: String, filterValue: String ): Array { var returnVal: Array = new Array( ); var numSoldiers: Number = this.soldiersList.length; var actualSoldierInfo: Object; for( var k: Number = 0; k< numSoldiers; k++ ) { actualSoldierInfo = this.soldiersList[ k ].getPublicInfo( ); switch( filterType ) { case Marshal.CONTAINS_LETTER: if( actualSoldierInfo.name.indexOf( filterValue ) != -1 ) { returnVal.push( this.soldiersList[ k ] ); } break; case Marshal.COMPANY: if( actualSoldierInfo.company == filterValue ) { returnVal.push( this.soldiersList[ k ] ); } break; default: trace( "Filter not created" ); } } return returnVal; } }
Y el Profesor hará algo como esto:
class Professor { function Professor( ) { } public static function initApp( ) { var professor : Professor = new Professor( ); var marshal : Marshal = new Marshal( ); marshal.recruitSoldier( new Soldier( { name: "Peter", id: "0001", company: "B" } ) ); marshal.recruitSoldier( new Soldier( { name: "Paul", id: "0002", company: "B" } ) ); marshal.recruitSoldier( new Soldier( { name: "Cesar", id: "0003", company: "A" } ) ); marshal.recruitSoldier( new Soldier( { name: "Javier", id: "0004", company: "B" } ) ); var companyB: Array = marshal.getFilteredCollection( Marshal.COMPANY, "B" ); trace( companyB ); var companyA: Array = marshal.getFilteredCollection( Marshal.COMPANY, "A" ); trace( companyA ); var nameWithP: Array = marshal.getFilteredCollection( Marshal.CONTAINS_LETTER, "P" ); trace( nameWithP ); var nameWithe: Array = marshal.getFilteredCollection( Marshal.CONTAINS_LETTER, "e" ); trace( nameWithe ); } }
Pero ¿qué pasa si ahora el Profesor quiere un desfile con los soldados cuyo número de serie sea menor que 10.000?. Habría que cambiar el código del Mariscal. ¿Y si ahora el Profesor quiere...? (ya sabes, al final siempre pasa lo mismo, los genios del mal no hacen más que pedir y pedir). El código del Mariscal, tal y como está no es muy escalable. No es facil añadir nuevas condiciones de filtrado. Pero ése no es el único problema. Con cada iteración del bucle, hay que chequear todas las posibles condiciones, lo cual es, por decirlo suavemente, poco elegante. Y el Profesor Dispar puede estar loco, pero desde luego no es idiota, y quiere que su código sea claro, compacto, fácil de mantener y elegante.
Así pues, el Profesor hace memoria, y recuerda (aquí es donde van los truenos y relámpagos) sus tiempos de estudiante, cuando aprendió el patrón (no, lo siento, el nombre lo tienes que adivinar tú).
Si encontrara una forma de darle al Mariscal un paquete que contuviera la condición a chequear, de forma que el Mariscal no necesitara conocer lo que está chequeando, el problema estaría resuelto. El Mariscal delegaría el chequeo de esa condición en lo que le entregue el Profesor, y se podrían añadir nuevas condiciones sin cambiar el código del Mariscal. Veamos cómo.
El Profesor le va a entregar, por tanto, al Mariscal una clase que será la responsable de realizar el filtro. Pero puede haber muchas condiciones, por lo tanto, todas las clases que se le puedan dar al Mariscal deberán implementar un interfaz común para que el Mariscal las pueda manejar de forma anónima.
Ese interfaz puede ser:
This interface could be:
interface ICompare { public function matches( checkValues: Object ): Boolean; }
Por tanto, uno de los filtros podría escribirse así:
class NameContainsLetterFilter implements ICompare { private var refValue: String; function NameContainsLetterFilter( ref: String ) { this.refValue = ref; } public function matches( checkValues: Object ): Boolean { return ( checkValues.name.indexOf( this.refValue ) != -1 ); } }
Y el código del Mariscal pasaría a ser:
class Marshal { private var soldiersList: Array; function Marshal( ) { this.soldiersList = new Array( ); } public function recruitSoldier( newSoldier: Soldier ) { this.soldiersList.push( newSoldier ); } public function getFilteredCollection( filter: ICompare ): Array { var returnVal: Array = new Array( ); var numSoldiers: Number = this.soldiersList.length; var actualSoldierInfo: Object; for( var k: Number = 0; k< numSoldiers; k++ ) { actualSoldierInfo = this.soldiersList[ k ].getPublicInfo( ); if( filter.matches( actualSoldierInfo ) ) { returnVal.push( this.soldiersList[ k ] ); } } return returnVal; } }
Y el Profesor Dispar:
class Professor { function Professor( ) { } public static function initApp( ) { var professor : Professor = new Professor( ); var marshal : Marshal = new Marshal( ); marshal.recruitSoldier( new Soldier( { name: "Peter", id: "0001", company: "B" } ) ); marshal.recruitSoldier( new Soldier( { name: "Paul", id: "0002", company: "B" } ) ); marshal.recruitSoldier( new Soldier( { name: "Cesar", id: "0003", company: "A" } ) ); marshal.recruitSoldier( new Soldier( { name: "Javier", id: "0004", company: "B" } ) ); var nameWithP: Array = marshal.getFilteredCollection( new NameContainsLetterFilter( "P" ) ); trace( nameWithP ); var nameWithe: Array = marshal.getFilteredCollection( new NameContainsLetterFilter( "e" ) ); trace( nameWithe ); } }
Añadir nuevos filtros será sencillo. Por ejemplo, para añadir un filtro utilizando el nombre de la compañía, el Profesor Dispar lo podría encapsular en una clase:
class CompanyFilter implements ICompare { var refValue: String; public function CompanyFilter( ref: String ) { this.refValue = ref; } public function matches( checkValues: Object ): Boolean { return ( checkValues.company == this.refValue ); } }
Otro filtro más:
class IDLowerFilter implements ICompare { private var refValue: String function IDLowerFilter( ref: String ) { this.refValue = ref; } public function matches( checkValues: Object ): Boolean { var ref : Number = parseInt( this.refValue, 10 ); var check : Number = parseInt( checkValues.id, 10 ); return ( check<= ref ); } }
Y para utilizar esos filtros:
class Professor { function Professor( ) { } public static function initApp( ) { var professor : Professor = new Professor( ); var marshal : Marshal = new Marshal( ); marshal.recruitSoldier( new Soldier( { name: "Peter", id: "0001", company: "B" } ) ); marshal.recruitSoldier( new Soldier( { name: "Paul", id: "0002", company: "B" } ) ); marshal.recruitSoldier( new Soldier( { name: "Cesar", id: "0003", company: "A" } ) ); marshal.recruitSoldier( new Soldier( { name: "Javier", id: "0004", company: "B" } ) ); var nameWithP: Array = marshal.getFilteredCollection( new NameContainsLetterFilter( "P" ) ); trace( nameWithP ); var nameWithe: Array = marshal.getFilteredCollection( new NameContainsLetterFilter( "e" ) ); trace( nameWithe ); var companyB: Array = marshal.getFilteredCollection( new CompanyFilter( "B" ) ); trace( companyB ); var companyA: Array = marshal.getFilteredCollection( new CompanyFilter( "A" ) ); trace( companyA ); var idLessThan2: Array = marshal.getFilteredCollection( new IDLowerFilter( "0002" ) ); trace( idLessThan2 ); var idLessThan3: Array = marshal.getFilteredCollection( new IDLowerFilter( "0003" ) ); trace( idLessThan3 ); } }
De esta forma, el Profesor ha sido capaz de cambiar los filtros sin cambiar el código del Mariscal.
Pero hay que hacer algunas consideraciones. Todos los filtros implementan un interfaz. Se podría discutir sobre si deberían extender de una clase base o no, pero el hacerlos extender de una misma clase base les haría perder flexibilidad. Por ejemplo, imagina que hay que realizar un filtro basado en cálculos numéricos... Por eso, aunque en este ejemplo podría ser válido el usar una clase base, el polimorfismo se basará en la implementación de un interfaz común.
Resumiendo, ¿qué ha conseguido el Profesor?. En primer lugar, ha seguido el Open-closed principle. El Mariscal está abierto a extensiones, pero cerrado a modificaciones. Dicho de otra forma, se puede extender su funcionalidad sin cambiar su código.
En segundo lugar, se ha delegado la construcción de los filtros en una entidad totalmente distinta a la que los ejecuta, es decir, se ha desacoplado totalmente la creación de esos filtros.
El profesor ríe histéricamente mientras la imagen se funde a negro... Pero ríe no sólo por la resolución de su aventura de hoy, sino porque sabe que este patrón se puede mejorar, por ejemplo, utilizando filtros que sean la composición de otros filtros... (continuará...)
Ahora te toca a tí. ¿Cómo se llama el patrón? (pista: el Profesor ha desacoplado un algoritmo de su receptor, encapsulándolo en una clase separada)
Descarga el código fuente. (si te apetece, claro)
Escrito por Cesar Tardaguila en: Octubre 17, 2005 07:25 AM | TrackBacksiempre muy interesante leer implementaciones de patrones en AS2. gracias por el post.
critica constructiva: me paso con post anteriores tambien, quise imprimirlos y se rompe todo el layout de la pagina. por lo menos en firefox.
Diego.
Gracias por el aviso, Diego. No nos habíamos dado cuenta de los problemas con la impresión. Lo ponemos a la cola para arreglarlo en cuanto sea posible.
Posted by: Cesar Tardaguila en: Noviembre 4, 2005 06:26 AM