21 de septiembre de 2015

Escribir código en métodos, no en eventos; pero ¿por qué?

Artículo original: Code in Methods, not Events. But why ???
http://weblogs.foxite.com/andykramek/2005/05/30/code-in-methods-not-events-but-why
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


Una de nuestras máximas favoritas es "siempre escribe el código en los métodos, no en los eventos" y yo siempre me hago exactamente la misma pregunta, ¿qué significa eso exactamente y por qué? Después de todo, VFP nos proporciona la clase CommandButton perfectamente programable, que tiene un evento Click() en el cual podemos colocar código que deseamos ejecutar al hacer Click sobre el botón. ¿Qué hay de malo en eso? La respuesta corta es que no hay nada malo en eso. Pero no es buena idea - he aquí algunas razones por las que no lo es.

Primero, y lo más obvio, es que el nombre del evento describe la situación en la cual se asocia el código a ser ejecutado, (ejemplo Click(), GotFocus(), Error(), etc. ) Sin embargo, no nos da ninguna indicación de qué es lo que ocurre cuando se dispara este evento y por lo tanto no es una vía fácil para luego examinar el código, para determinar que es lo que está mal. Considere el siguiente código que debe ser llamado cuando hace click sobre un botón en particular.

LOCAL lcSceDir, lcDstDir, lcQuoDir, lnCnt, lcToDo, loErr
WITH ThisForm
  *** Pedir las localizaciones de las carpetas
  lcSceDir = .txtSceDir.Value
  lcDstDir = .txtDestDir.Value
  lcQuoDir = .txtQuotDir.Value
  .oMig = NEWOBJECT('xMigrator', 'datamigrator.prg', null, lcSceDir, lcDstDir, lcQuoDir, .T. )
  IF VARTYPE( .oMig ) # "O"
    RETURN
  ELSE
    .UpdProgress( 'La inicialización comienza en ' + TIME())
    .oMig.oParent = ThisForm
  ENDIF
  FOR lnCnt = 1 TO ALEN( .aProcs, 1 )
    IF .aProcs[ lnCnt, 2 ]
      lcToDo = .aProcs[ lnCnt, 1 ]
      .UpdProgress( 'Start Process [' + lcToDo + '] at ' + TIME())
      .oMig.RunProcess( lcToDo )
      .UpdProgress( 'Ended Process [' + lcToDo + '] at ' + TIME())
    ENDIF
  NEXT  
  loErr = .oMig.GetErrors()
  IF loErr.nerrors > 0
     loErr.ShowErrors()
  ENDIF
ENDWITH

Tuve que quitar deliberadamente todos los comentarios de este bloque; pero es posible inferir que es algo relativo a la migración de datos. Contraste eso con el código que en realidad aparece en el evento Click de mi CommandButton

ThisForm.RunMigration()

¿Ve la diferencia? Ahora es perfectamente obvio qué es lo que ocurre cuando se hace clic sobre este botón de comandos; un método llamado "RunMigration" es llamado en su clase padre.

La segunda razón para no colocar código directamente en un evento es relativa a la primera. En el ejemplo que yo tengo, es extremadamente improbable que el método RunMigration() vaya a ser llamado desde otro lugar, excepto del botón de comandos en un formulario. Sin embargo, ¿qué hay sobre el código que agrega un registro a una tabla? ¿O el código para controlar el guardar o revertir los cambios? Este código es completamente genérico (por ejemplo, el código no depende de cuál de las tablas es la seleccionada cuando están usando comandos y funciones tipo APPEND BLANK, TABLEUPDATE() o TABLEREVERT() - asumiendo, por supuesto que es el alias correcto para hacerlo).

He aquí dos soluciones que veo generalmente en el código. La primera es que simplemente cada formulario (y frecuentemente cada página de formulario!) tenga su propio botón de comando Guardar con el código que se muestra a continuación en su evento Click()

IF NOT TABLEUPDATE( 2, .F., ‘customer’)
  MESSAGEBOX( "No se pueden guardar los cambios", 16, "Fallo en el sistema" )
ENDIF

La segunda razón es cuando se crea una clase genérica "Botón Guardar" que es creada con el siguiente código:

*** Debemos estar en el área de trabajo correcta
IF NOT EMPTY( ALIAS() )
  IF NOT TABLEUPDATE( 2, .F., ALIAS())
  MESSAGEBOX( "No se pueden guardar los cambios", 16, "Fallo en el sistema" )
  ENDIF
ENDIF

Entonces, nos tenemos que preguntar nuevamente, ¿dónde debe estar el código? Claramente, en pocos lugares lo mejor será la primera solución (escribir directamente el código en el evento Click() no es bueno). La idea de utilizar una clase es mejor; pero la cuestión en esto, como en muchas otras cuestiones en POO es ¿Dónde descansa la responsabilidad?

Para ponerlo de otra forma, ¿es realmente trabajo del botón de comandos, controlar la actualización de la tabla? Yo diría ¡ definitivamente, no! La función de un botón de comandos es informar al sistema de que el usuario quiere hacer algo - pero no significa que el botón de comando es el mejor lugar para HACER cualquier cosa que se requiera y el código para guardar es un buen ejemplo en este punto. Obviamente, el objeto que es responsable de guardar los datos del formulario, es el formulario, entonces ¿no tendría sentido guardar allí su código? Si todo esto es cierto, si se requiere entonces el código que mostré antes, vaya en un método sencillo en la clase formulario y simplemente heredar por todas los formularios basados en esta clase.

Una vez que el código está en el formulario no necesitaremos preocuparnos en pensar desde dónde será llamado. Asumiendo que el objeto que llama es el responsable de establecer el área de trabajo, podemos llamar ThisForm.SaveData() desde cualquier control, en cualquier lugar del formulario y aun saber que es lo que ocurre cuando revisemos el código 6 meses después.

La tercera razón para evadir la colocación de código directamente en un evento, a favor de colocarlo en un método, es la relativa a la naturaleza de los métodos y eventos. La diferencia fundamental entre un método en VFP y un evento, está en realidad en cómo y cuándo son llamados. Un método puede ser llamado explícitamente, en código con la adecuada referencia al objeto donde ha sido definido, así:

ThisForm.Refresh()
_Screen.ActiveForm.AddProperty( ‘junkprop’, NULL )

Este no es el caso para los eventos. Un evento se dispara como resultado de alguna acción y la llamada de un método del mismo nombre en realidad no dispara un evento. Entonces el código que ejecuta cuando se hace clic sobre el botón de comandos se puede disparar de dos formas:

Explícitamente, al llamar por código al método Click() del botón. No obstante aunque se ejecuta el código, no significa que se dispara el evento.

Implícitamente como resultado de que el usuario haga Clic sobre el botón, se dispara el evento y se ejecuta el código asociado.

Posiblemente, la vía más sencilla para ilustrar esto es con un evento que se utiliza con frecuencia, pero que pocas veces se invoca por código - KeyPress(). Arrastre un textbox base a un formulario y agregue el siguiente código a su evento KeyPress()

LPARAMETERS nKeyCode, nShiftAltCtrl
IF VARTYPE( nKeyCode ) = "N"
  WAIT "Esto fue llamado como un EVENTO" WINDOW 
ELSE
  WAIT "Esto fue llamado como un METODO" WINDOW 
ENDIF  

Ahora, añada un botón de comandos y en su evento click (si, lo se...) sólo agregue:

ThisForm.Text1.Value = "A"
ThisForm.Text1.KeyPress()

Al ejecutar el formulario verá que al oprimir la tecla "A" en el textbox se dispara el evento KeyPress() y por tanto tiene el mensaje  "EVENTO". Sin embargo, si hace clic sobre el botón, incluso agrega la letra A en el textbox (creando por supuesto el mismo resultado que si la hubiera tecleado por si mismo)-, pero el evento KeyPress() no se dispara! Es más, llamar al evento KeyPress() hace que se ejecute el código que está en el evento; pero, como no se han pasado correctamente los parámetros sólo se verá MÉTODO en la ventana del mensaje.

Por supuesto, podríamos simular que el evento pase explícitamente los parámetros correctos; pero este no es el punto. El punto es que el evento siempre se va a disparar cuando en realidad ocurra - queramos o no. He escogido deliberadamente KeyPress porque veo con mucha frecuencia código como ese en ese evento:

DO CASE
  CASE nKeyCode = 18 OR nKeyCode = 57 OR nKeyCode = 31 OR nKeyCode = 153 && Page Up
       *** Hacer algo especial y luego desactivar el procedimiento nativo
    NODEFAULT   
  CASE nKeyCode = 3 OR nKeyCode = 51 OR nKeyCode = 30 OR nKeyCode = 161  && Page Dn
       *** Hacer algo especial y luego desactivar el procedimiento nativo
    NODEFAULT   
  CASE nKeyCode = 5 && Up Arrow
       etc etc etc ...
  OTHERWISE
       *** No son teclas de navegación - dejar que lo procese
ENDCASE

A mi me parece una vía muy ineficiente de procesar las pulsaciones sobre el teclado (recuerde que este evento se va a disparar luego de cada pulsación). Probablemente DO CASE sea la peor opción para este tipo de procesos y es relativamente lento. Una solución mejor puede ser utilizar una función INLIST() que llame y redireccione la ejecución a un método si se ha pulsado una tecla que es de nuestro interés. Entonces, el método se podría reescribir de esta forma:

IF INLIST( nKeyCode, 18,57,31,153,3,51,30,161,5 )
  ThisForm.ControlaTeclas( nKeyCode )
  NODEFAULT
ENDIF

Independientemente de si es más rápido o no, a mi me hace sentir mejor, no sólo debido a que sigue el hilo del código desde un evento, sino además, porque es mucho más fácil de ver que es lo que ocurre. Está claro que algunas teclas serán interceptadas y se les pasará algún código especial. Lo que no es inmediatamente obvio es colocar una instrucción DO CASE embebida en un evento KeyPress() de cualquier control.

El beneficio adicional es, que si necesitamos una forma diferente para manipular las mismas teclas es vías diferentes, no tendremos que hacer cambio alguno en el código de nuestros controles - solamente en el método como tal. Mucho más limpio.

Como siempre, no existe una forma absolutamente "correcta" o "incorrecta" para hacer este tipo de cosas; pero yo estoy firmemente convencido de que, mantenerlo lo más sencillo posible, teniendo mi código bajo mi propio control, donde yo sepa que es lo que se hace y cuando es llamado, es la forma más sencilla. Para mi, está claro, debido a que de esta forma, es más fácil centralizar y mantener mi código. Pero... si aun encuentra alguna razón para escribir su código de la siguiente forma, es posible que sea el momento de volver a pensar ....

ThisForm.PageFrame1.Page1.Container1.OptionGroup1.Optionbutton1.Click()

¡A pasarla bien!


No hay comentarios. :

Publicar un comentario