« ¿En defensa de la familia? | Inicio | Google earth gratuito »

No te repitas, y encapsula lo que pueda cambiar

No te repitas. En cuanto escribes el mismo código más de una vez, estás empezando a meterte en problemas

Aquí tienes un ejemplo.

Estoy trabajando en una aplicación en la que voy a utilizar los componentes de UI. Es una aplicación Modelo-Vista-Controlador, por lo que la vista estará totalmente desacoplada de la lógica de la aplicación

La aplicación va a tener más de una vista ( hay varias "pantallas" diferentes en las que tengo que mostrar los datos de diferentes formas ), por lo que he decidido que lo mejor será escribir una clase diferente para cada una de esas vistas.

Por tanto, cada vista attacheará un movieclip, que es el que contiene realmente la "pantalla" con los elementos interactivos ( botones, datagrid, etc ). Todas esas vistas extienden de una clase que implementa cierta funcionalidad común ( emisión de eventos y alguna cosilla más ). Por tanto, no extenderán de movieclip ( aparte de por razones un poco más abstractas, aunque éste no es el momento para discutir eso ). A lo que vamos.

Además, resulta ( todo son facilidades ) que por ahora no tengo los gráficos definitivos ( vamos, que me lo estoy pintando yo ). Aún más, si al final el presupuesto da para ello, tendré que desarrollar mis propios componentes.

Bien, pues una vez puestos en antecedentes, imagina que arranca la aplicación. Lo primero con lo que se va a encontrar el usuario es con una pantalla de login

encapsulate.jpg

Como puedes ver en la captura, a) más me vale que me vaya bien en esto de la programación, por que lo que es en la parte gráfica...; b) la pantalla contiene un componente ( el botón ).

La clase que maneja esa pantalla será:

import mx.utils.Delegate; class LoginView { private var timeline : MovieClip; private var loginScreen: MovieClip; function LoginView( tl: MovieClip ) { this.timeline = tl; } public function init( ) { this.loginScreen = this.timeline.attachMovie( "LoginScreen", "LoginScreen", this.timeline.getNextHighestDepth( ), { _x: 0, _y: 0 } ); this.loginScreen.loginBtn.label= "Go!"; this.loginScreen.loginBtn.addEventListener( "click", Delegate.create( this, click ) ); } private function click( ) { trace( "click" ); } }

Y cuando quiera que aparezca, haré algo como:

var loginView: LoginView = new LoginView( this ); loginView.init( );

Y el botón no va a funcionar. Ni siquiera va a mostrar el texto que le he asignado. Tras unos minutos de buscar en google, un poco de sentido común, y un poco menos de conocimiento de la plataforma, he llegado a la conclusión de que el botón no se inicializa a tiempo, y que tal vez debería dejar pasar un frame para asignar el texto y el click.

import mx.utils.Delegate; class View { private var timeline : MovieClip; private var loginScreen: MovieClip; function View( tl: MovieClip ) { this.timeline = tl; } public function init( ) { this.loginScreen = this.timeline.attachMovie( "LoginScreen", "LoginScreen", this.timeline.getNextHighestDepth( ), { _x: 0, _y: 0 } ); var theView: View = this; this.loginScreen.onEnterFrame = function( ) { theView.initButton( ); delete this.onEnterFrame; } } private function initButton( ) { this.loginScreen.loginBtn.label= "Go!"; this.loginScreen.loginBtn.addEventListener( "click", Delegate.create( this, click ) ); } private function click( ) { trace( "click" ); } }

Y funciona. Pero esta solución no huele demasiado bien ( y no sólo por el truqui del scope ). Voy a tener que hacer bastantes pantallas ( alrededor de una docena ), así que voy a tener que repetir ese código en todas ellas. Desde luego, podría subclasificar, pasar ese código a una clase base, pero no voy a tener el mismo número de botones en todas las pantallas, luego tampoco tendría mucho sentido subclasificar para luegosobreescribir, y además, sólo podría utilizar ese código en subclases de la clase base que creara, con lo cual no iba a tener demasiada flexibilidad, que digamos. Y además, mis vistas ya extienden de otra clase base.

E incluso puede ser peor, porque es probable que tenga que cambiar esos componentes por unos míos ( o no, que aún no lo sé ). Así que es más que probable que ese código tenga que cambiar en un futuro no muy lejano

Así pues, estoy repitiendo código, y además, es probable que ese código tenga que cambiarlo en breve. ¿Cómo lo puedo resolver?. Pues encapsulando ese código dentro de su propia clase.

Para empezar, usaré un callback que ya he utilizado más veces:

class Callback { private var callbackObjVal: Object; private var callbackMethodVal: String; public function Callback( objParam: Object, methodParam: String ) { this.callbackObjVal = objParam; this.callbackMethodVal = methodParam; } public function fire( parameter: Object ): Object { return this.callbackObjVal[ this.callbackMethodVal ]( parameter ); } }

La clase encargada de manejar el botón será:

class ButtonHandler { public static function initButton( mc: MovieClip, callback: Callback ) { var cb: Callback = callback; mc.onEnterFrame = function( ) { cb.fire( ); delete this.onEnterFrame; } } }

Y la utilizaré de la siguiente forma:

import mx.utils.Delegate; class View { private var timeline : MovieClip; private var loginScreen: MovieClip; function View( tl: MovieClip ) { this.timeline = tl; } public function init( ) { this.loginScreen = this.timeline.attachMovie( "LoginScreen", "LoginScreen", this.timeline.getNextHighestDepth( ), { _x: 0, _y: 0 } ); ButtonHandler.initButton( this.loginScreen, new Callback( this, "initButton" ) ); } private function initButton( ) { this.loginScreen.loginBtn.label= "Go!"; this.loginScreen.loginBtn.addEventListener( "click", Delegate.create( this, click ) ); } private function click( ) { trace( "click" ); } }

¿Una solución excesivamente compleja?. Puede, pero es la que más me ha satisfecho, porque es la que aporta más flexibilidad. Si tengo que cambiar la forma en la que manejo los botones, sólo tengo que cambiar la implementación de un método en una clase. Estoy programando pensando en el interfaz, y no en la implementación.

Comentarios

Efectivamente, tanto attachMovie() como loadMovie() son métodos asíncronos.

también lo podías haber solucionado más sencillo asi, :

public function init( )
{
this.loginScreen = this.timeline.attachMovie( "LoginScreen", ...);


this.time.onLoad = function(){
this.loginScreen.loginBtn.label= "Go!";
this.loginScreen.loginBtn.addEventListener( "click", Delegate.create( this, click ) );
}

}

Perdona, me he colado mandandote el comentario anterior varias veces.

Pensando sobre loadmovie() y attachmovie() como asincronos, me viene una duda:

Si un MovieClip añadido con AttachMovie() crea a su vez otros MC´s dentro de el con el mismo metodo AttachMovie(), el evento onLoad del primero se lanza cuando todos los hijos han lanzado recursivamente un evento onLoad()?

Si no fuese así podría haber 'hijos de los hijos' no inicializados en el momento que se dispara onLoad().

¿Que opinas de esto?

Saludos

Pues la verdad, no sé muy bien lo que pasaría cuando haya más clips dentro del primer clip, pero todo es probar.

En cuanto a la solución de onLoad, efectivamente, es posible, pero no es muy distinta a la de dejar pasar un frame, porque habría que seguir copiando y pegando ese código en varias clases, y el manejo del botón seguiría sin estar encapsulado, y sin ser muy fácil de cambiar.

En todo caso, es una solución al problema de la inicialización del componente que no se me había ocurrido probar. Gracias!

Hola Cesar,

Solo quería aportar mi granito de arena recordando que si se usa el framework v2 de Macromedia (...aunque ya se que estás pensando en una situación generica, pero en caso de que uses finalmente v2), existe el método doLater, que permite ejecutar código una vez que la interface se ha actualizado.

Saludos :)

En lugar de hacer un onEnterFrame, que funciona, puedes usar doLater para inicializar los componentes. Al método init de cada vista, que por lo que pones deberás llamarla cada vez que crees una vista, podrías pasarle los datos para inicializar los componentes, y emplear el método doLater en dicha función de cada vista.

Se me ha olvidado un artículo sobre eso mismo: http://www.markme.com/pent/archives/007731.cfm
;)

Efectivamente, se puede utilizar el doLater para saber cuándo se ha inicializado el botón, pero a estas alturas, no estoy seguro de si el botón va a ser uno de los componentes del framework V2 o va a tener que ser mi propio componente, es decir, es probable que la forma de manejar esa inicialización vaya a cambiar.

De ahí la idea de encapsular esa inicialización en su propia clase...

La idea del post es que si hay algo que preveemos que puede cambiar, hay que intentar no repetirlo (y en general hay que intentar no repetir código ), y a ser posible, encapsularlo en una clase para que se pueda modificar sin afectar al resto del programa.