El objetivo:
Mi objetivo es mostrar un ejemplo de implementación del modelo-vista-controlador, que sirva para ilustrar algunas de las novedades más importantes en la última revisión (aunque en este caso casi debería hablarse de reinvención) de ActionScript.
Para ello, voy a implementar uno de las dos aplicaciones de ejemplo que solemos utilizar habitualmente: un reloj. En este caso, un reloj muy sencillo, que va a mostrar una cuenta creciente de segundos, pero que también va a proveer un botón para poder pausar su operación. Por favor, tómese la aplicación como una prueba de concepto, como un vehículo, una herramienta para intentar facilitar la comprensión de lo verdaderamente importante, el idiom.
Como puede verse en la imagen, el desarrollo de interfaces no es el punto fuerte de los aquí presentes.
La arquitectura
El modelo-vista-controlador se ha revisado por aquí con anterioridad. Para no repetirme demasiado, simplemente me voy a permitir mostrar el siguiente gráfico que permite comprender en su totalidad la arquitectura:
El concepto es sencillo. Se divide la aplicación en tres capas. Una de ellas (la vista) será la encargada de gestionar la interfaz y todo lo relacionado con la interacción entre la aplicación y el usuario, otra (el modelo) será la encargada de hacer el trabajo real de la aplicación, y la última (el controlador) sirve para desacoplar ambas, de forma que, si fuera necesario, una vista pueda ser actualizada por varios modelos, o un modelo pueda proveer de datos varias vistas.
La comunicación entre el controlador y la vista se hace por push en ambos sentidos. Entre el modelo y el controlador, sin embargo, se hace por push cuando es el controlador el que necesita del modelo, y por eventos cuando es el modelo el que quiere notificar algo a la(s) vistas.
Se puede discutir sobre si ésta implementación es la canónica del mvc o no, pero, sinceramente, eso es, por un lado, tema para otro post diferente, y por otro, una discusión similar a la del sexo de los ángeles. Mi opinión personal es que los patrones, como prácticamente todo en esta vida, desde la ropa al sillín de la bicicleta, deben adaptarse a las necesidades y gustos del que los utiliza. En mi caso, ésta es la implementación que más me satisface, la que mejores resultados me da, la que más fácilmente puedo implementar en diversos lenguajes, y por tanto, la que yo utilizo.
Los detalles de implementación
De la arquitectura anterior puede desprenderse que la cadena de eventos de la aplicación será la siguiente:
- Creación del controlador
- Creación de la vista, y paso de una referencia de la misma al controlador
- Creación del modelo desde el controlador
- Inicialización del sistema de eventos para las notificaciones emitidas por el modelo
- Arranque del modelo.
El controlador, por tanto, dada su función de capa de abstracción entre el modelo y la vista, deberá mantener una referencia a ambos. Además, deberá ser capaz de escuchar los eventos emitidos por el modelo.
La gestión de eventos
Una de las novedades de AS3 es la forma en la que se gestionan los eventos. Por fin, se ha racionalizado y estandarizado, y ya no hace necesario escribir a mano un sistema propio y racional de gestión de eventos, como ocurría hasta la fecha.
Básicamente, todo evento que se quiera emitir, debe extender de la clase flash.events.Event
La clase en la que especialice el evento a emitir simplemente debe llamar al construcor de su superclase, pasándole como parámetro una cadena de texto con el nombre por el que se quiera reconocer al evento creado.
Por tanto, en el ejemplo del reloj que estoy construyendo, si quisiera que se emitiera un evento desde el modelo, con el valor del tiempo transcurrido en el reloj hasta ese momento, lo podría hacer de la siguiente manera:
En el constructor de mi evento, paso dos valores como parámetro: el nombre del evento para el que voy a utilizar la instancia concreta de la clase, y el valor que quiero encapsular en dicho evento. Para facilitar en lo posible ese encapsulamiento, también proporciono un método público (getValue( ) ) que me permitirá utilizar ese valor en otros ámbitos de mi aplicación.
Paso el nombre del evento como parámetro, para, fundamentalmente, ilustrar mi ejemplo. En cualquier caso, al arrancar el modelo de la aplicación, voy a emitir un evento con un nombre distinto al que emitiré con cada pulsación del reloj. ¿Por qué? Básicamente porque me apetece, porque no me parece deban manejarse la inicialización del reloj y las pulsaciones del mismo con el mismo evento, porque no son la misma cosa.
Por eso, en el modelo voy a declarar dos constantes diferentes que voy a utilizar para crear los dos eventos. Puede no ser el mejor lugar para hacerlo, ciertamente, pero creo que para un ejemplo como éste, con sólo dos eventos, no es necesario complicarse más:
En la implementación del método start( ) (el que se manda ejecutar desde el controlador cuando se arranca la aplicación) puede verse la forma en la que se crea el evento llamado onStartEvent, que puedo construir de dos maneras: utilizando una instancia de la clase TickEvent, y pasando a esa clase el valor de counter en ese momento (que al iniciarse el reloj es cero), o simplemente como un evento genérico, sin más especialización de su nombre. En este caso, sigo el segundo camino, y hago que sea la vista la que presente un cero como valor inicial. ¿Mal hecho? Sí. Pero como soy consciente de haberlo hecho mal, siempre lo puedo refactorizar...
En las dos últimas líneas de la captura anterior se pude observar cómo funciona el mecanismo de escucha de eventos, que puede verse como una mezcla de las delegaciones y de la escucha de eventos utilizada en AS2. Al registrarse a un emisor de eventos, se elige el nombre del evento a escuchar (en este caso, el declarado en la clase flash.utils.Timer con la constante TIMER) y se elige el callback que escuchará el evento (en este caso el método onTimeTick).
Al recibirse la notificación del evento, que es del tipo TimerEvent, el modelo de la aplicación emite otro evento, hacia aquellos que se hayan registrado para escucharle, que es una instancia de TickEvent, esta vez, con nombre onTimeTick.
Para que el modelo pueda emitir eventos, debe extender de flash.events.EventDispatcher, como así ocurre.
El controlador, que ha sido el responsable de crear el modelo, está registrado a los eventos que éste emite:
Eventos a los que se registra, utilizando para identificarlos las constantes que contienen sus nombres, y que fueron declaradas en el modelo.
El resto del flujo de eventos de la aplicación es sencillo. Con cada pulsación del reloj agregado al modelo (la instancia de flash.utils.Timer), se emite un evento que recibe el controlador, evento del que se extrae el valor del temporizador, para ser trasladado a la vista, que es la que lo presenta en pantalla:
La vista dibuja un gráfico para ser utilizado como botón. Como ese gráfico sólo se va a utilizar para capturar los clics que se hagan sobre él, no tiene sentido que sea un movieclip, sino que basta con que sea una instancia de Sprite:
Como puede verse, la forma de gestionar los eventos de ratón es la misma que con el resto de eventos. Una vez creados el Sprite y un campo de texto para presentar el valor del temporizador, se añaden al dom, y listo.
¿Qué se ha conseguido con esta arquitectura? Aislar la funcionalidad real de la aplicación (el temporizador) en una sola capa, en la que no se maneja nada relacionado con la interfaz ni con la interacción con el usuario. De esa forma, se tiene bien encapsulada esa funcionalidad, y se gana en modularidad, portabilidad y cohesión mientras que se reduce el acoplamiento con el interfaz. ¿Que se quiere cambiar completamente al interfaz para hacer que parezca un reloj de manecillas? Ningún problema, se cambia la vista y solucionado.
El código completo del ejemplo puede descargarse en este enlace. Debería compilar sin problemas con Flash CS3. Trastea con él, léelo, verás que no es tan complicado como lo he hecho sonar.