domingo, octubre 12, 2014

VFP: La Interfaz, las reglas de negocio y los Datos - Cómo separarlos y por qué

Por: Fernando D. Bozzo

Actualizaciones:
03/06/2015 - Agregada nueva sección de Posibles mejoras al modelo propuesto
04/06/2015 - Agregada nueva sección sobre ¿y el control de errores?

Cuando se quiere actualizar alguna propiedad de un control, como Enabled, ReadOnly, BackColor, ForeColor u otra, dependiendo del valor de uno o más controles, normalmente los programadores hacen esas actualizaciones desde los mismos controles.

Por ejemplo, si tenemos un form donde se debe activar el botón cmdImprimir solamente si se pone un código (txtCodArticulo) y un valor (txtValor), muchos harán algo como esto:


Ejemplo 1:

Form1.txtCodArt.Valid
IF NOT EMPTY(this.value) AND NOT EMPTY(thisform.txtValor.value)
  THISFORM.cmdImprimir.Enabled = .T.
ELSE
  THISFORM.cmdImprimir.Enabled = .F.
ENDIF

Form1.txtValor.Valid
IF NOT EMPTY(this.value) AND NOT EMPTY(thisform.txtCodArt.value)
  THISFORM.cmdImprimir.Enabled = .T.
ELSE
  THISFORM.cmdImprimir.Enabled = .F.
ENDIF

Form1.Activate
IF NOT EMPTY(thisform.txtCodArt.value) AND NOT EMPTY(thisform.txtValor.value)
  THISFORM.cmdImprimir.Enabled = .T.
ELSE
  THISFORM.cmdImprimir.Enabled = .F.
ENDIF



¿Qué problema hay con el código de arriba, si al final funciona?

El problema es que no importa solamente que "funcione", sino que además el código sea fácil de mantener, de modificar, de ampliar y de contemplar nuevos casos posibles, como veremos luego.

Aunque en un principio pueda parecer que lo de "copiar/pegar/adaptar" es mucho más fácil y rápido, luego, al tener que modificar todo lo "copiado/pegado/adaptado" no lo será, y cada vez será más difícil, lo que nos dejará con "Código Spaguetti", que se llama a esos bloques de código dispersos por ahí, duplicados y difíciles de mantener.



¿Y cómo lograr todo eso a la vez? ¿No será muy complicado?

Evalúenlo ustedes mismos, y comparen lo de arriba del ejemplo 1 con esto:


Ejemplo 2:

Form1.txtCodArt.Valid
THISFORM.EvaluarEstadosDeHabilitacion()

Form1.txtValor.Valid
THISFORM.EvaluarEstadosDeHabilitacion()

Form1.Activate
THISFORM.EvaluarEstadosDeHabilitacion()

Form1.EvaluarEstadosDeHabilitacion
IF NOT EMPTY(thisform.txtCodArt.value) AND NOT EMPTY(thisform.txtValor.value)
  THISFORM.cmdImprimir.Enabled = .T.
ELSE
  THISFORM.cmdImprimir.Enabled = .F.
ENDIF



Demostración rápida del coste de mantenimiento:

Si en el Ejemplo 1 hubiera que agregar una condición más para activar el botón cmdImprimir, habría que modificar los 3 métodos y además volver a copiar y adaptar el código de uno de ellos para el nuevo control que interviene en esa condición, lo que nos deja con ¡4 versiones distintas de código que hacen lo mismo!

En el Ejemplo 2, agregar esa condición implica solo 2 cambios: el primero es poner THISFORM.EvaluarEstadosDeHabilitacion() en el nuevo control, y el segundo es agregar la condición en el método EvaluarEstadosDeHabilitacion.

¿Y si hubiera que agregar 50 controles más? Con el Ejemplo 1 esa tarea podría llevar toda la tarde y con una posibilidad de error muy alta, mientras que con el Ejemplo 2 sería cosa de 10 minutos máximo!


Ahora preguntémonos: ¿cómo es mejor, de la primera forma o de la segunda? La primera es una mala práctica y la segunda es una buena práctica porque encapsula la condición en un solo sitio, que es de lo que se trata al hacer cualquier programa o sistema, y eso que solamente hemos hablado de algo tan simple como un Enabled.



Técnicas de encapsulación - Trabajo en N capas


Si trabajamos en capas (negocio, datos, Interfaz) o con separación de responsabilidades, el Ejemplo 2 de antes se puede hacer de una forma parecida, que requiere algo más de código y de diseño, pero que al final también tiene más ventajas.

Una de las primeras separaciones que conviene hacer es la de los datos y la interfaz. En FoxPro tenemos tan arraigada esta mezcla interfaz+datos, que muy pocos se la cuestionan. Me refiero a no usar el ControlSource de los controles, o a usarlo apuntando a propiedades del mismo form, como se hace habitualmente.


Este es un caso de uso típico:

Form1.txtCodArt.Valid
IF EMPTY(this.value)
  MESSAGEBOX( "EL CÓDIGO DE ARTICULO NO PUEDE QUEDAR VACÍO" )
ENDIF

Form1.txtValor.Valid
IF EMPTY(this.value)
  MESSAGEBOX( "EL VALOR NO PUEDE QUEDAR VACÍO" )
ENDIF



El ControlSource de los controles conviene apuntarlo a propiedades de un objeto externo, de forma que ese objeto luego se puede reutilizar o incluso pasar como parámetro.


a) Creando un Objeto de negocio

Usando el primer ejemplo, nuestro objeto de negocio debería contener 2 propiedades, codArt y Valor:


*--  ARTICULO_BUS.PRG 
Define Class cl_articulo As Custom
   c_CodArt = ""
   n_Valor  = 0
EndDefine



b) Instanciando la clase de negocio en el form


En el form creamos una propiedad oBus y la cargamos con el objeto antes definido, agregándolo en el LOAD:


Form1.LOAD
THISFORM.oBus = NEWOBJECT( "cl_articulo", "articulo_bus.prg" )


c) Definiendo el nuevo ControlSource


Luego definimos los ControlSource de los controles en modo diseño:

txtCodArt.ControlSource = "thisform.oBus.c_CodArt"
txtValor.ControlSource = "thisform.oBus.n_Valor"



d) Referenciando los datos


Finalmente, modificamos el método del Ejemplo 2 para que no tome los valores directamente de los controles (nunca hay que tomar los datos de los controles!), sino de las propiedades del nuevo objeto:


Form1.EvaluarEstadosDeHabilitacion
LOCAL loBus As cl_articulo OF "articulo_bus.prg"
loBus = thisform.oBus

IF NOT EMPTY(loBus.c_CodArt) AND NOT EMPTY(loBus.n_Valor)
  THISFORM.cmdImprimir.Enabled = .T.
ELSE
  THISFORM.cmdImprimir.Enabled = .F.
ENDIF



e) Ampliando el uso del objeto de negocio: Validaciones


Supongamos que queremos agregar validaciones de datos, esas que normalmente se ponen en el VALID del control (como los ejemplos de más arriba), y que en ciertos casos terminan duplicándose o incluso cayendo en algo tan feo como teniendo que llamar desde un control al VALID de otros controles, o peor, un Valid llamando al InteractiveChange de otro control que a su vez llame al evento de otro control, y así sucesivamente...   Obviamente que eso no es una buena práctica, aunque sean muchos los que lo sigan haciendo.

Hay mejores formas y más limpias y fáciles de mantener para eso, como ser el poner esas validaciones en nuestro objeto de negocio:

*--  ARTICULO_BUS.PRG 
Define Class cl_articulo As Custom
   c_CodArt = ""
   n_Valor  = 0

   PROCEDURE EvaluarError_CodArt
      LOCAL lcCodError
      lcCodError = ""

      IF EMPTY(THIS.c_CodArt)
         lcCodError = "EL CÓDIGO DE ARTICULO NO PUEDE QUEDAR VACÍO"
      ENDIF

      RETURN lcCodError
   ENDPROC

   PROCEDURE EvaluarError_Valor
      LOCAL lcCodError
      lcCodError = ""

      IF EMPTY(THIS.n_Valor)
         lcCodError = "EL VALOR NO PUEDE QUEDAR VACÍO"
      ENDIF

      RETURN lcCodError
   ENDPROC

EndDefine

Nota: Como puede verse, las validaciones de negocio "devuelven" valores, cadenas o incluso arrays de valores, pero nunca muestran mensajes, ni siquiera de error, ya que eso es responsabilidad de la Interfaz (el form o sus controles)


Recuerden:

Al trabajar en capas, las validaciones no son responsabilidad de la Interfaz (pantallas), sino del Negocio, al igual que los cálculos y los procesos. En esta metodología de trabajo las pantallas solo deben contener el código necesario para la navegación, activación, desactivación o cualquier cosa específica de la Interfaz, y para el resto debe llamar a métodos de negocio.


Para usar estas validaciones, el formulario podría tener esto:

Form1.txtCodArt.Valid
THISFORM.EvaluarEstadosDeHabilitacion()
THISFORM.MostrarMensajeError( THIS.oBus.EvaluarError_CodArt() )

Form1.txtValor.Valid
THISFORM.EvaluarEstadosDeHabilitacion()
THISFORM.MostrarMensajesError( THIS.oBus.EvaluarError_Valor() )

Form1.MostrarMensajesError
LPARAMETERS tcMensajeError
IF NOT EMPTY(tcMensajeError)
   MESSAGEBOX( tcMensajeError )
ENDIF



Posibles mejoras en el esquema de validaciones


La forma de validar anterior puede ser útil cuando se quiere que los mensajes sean invasivos y bloqueantes, como se ha hecho por muchos años, pero puede resultar muy molesto hacerlo así en un formulario donde se quiere que el usuario tenga la mayor libertad posible para navegar por los controles sin que le vayan apareciendo mensajes a cada paso.

Para este caso, donde se quiere que el usuario vea los errores cuando realmente quiera verlos, hay varias alternativas.


Alternativa 1 - Mostrar mensajes en un panel especial


La idea es dejar un espacio reservado para el mensaje de error actual, por ejemplo un editbox en la parte inferior del form, de 2 ó 3 líneas de altura, donde el usuario pueda siempre ir viendo los mensajes a medida que recorre los controles. Para actualizar esa zona especial se podría adaptar el método MostrarMensajesError del ejemplo anterior, por ejemplo así:

Form1.MostrarMensajesError
LPARAMETERS tcMensajeError
IF EMPTY(tcMensajeError)
   THISFORM.edtMensajes.VALUE = ""
ELSE
   THISFORM.edtMensajes.VALUE = tcMensajeError
ENDIF

Observen lo simple que fue adaptar este método centralizado de evaluación de mensajes para cambiarle la implementación de cómo mostrar los mensajes, sin haber tenido que tocar ni un control.

Un ejemplo de este tipo de validación:




Alternativa 2 - Mostrar una ventana independiente de mensajes


En este caso la idea es que el usuario pueda ver todos los mensajes de validación juntos en una ventana separada o dentro del form y mostrados en formato tabular, por ejemplo con un grid o un ListBox.

Para ello, podríamos poner un botón "Ver Validaciones" en el form, cuyo evento click tenga esto:

Form1.cmdVerValidaciones.Click
THISFORM.oBus.ObtenerValidaciones(@laMensajes)
DO FORM MostrarMensajes WITH laMensajes


Y el form podría mostrarlos de forma similar a esto:




Alternativa 3 - Mensajes de validación en un proceso masivo


En el caso de un proceso en lotes, o masivo, donde no se muestran mensajes, sino que se loguean para posterior análisis, se podría usar el método oBus.ObtenerValidaciones() desde un proceso externo, y guardar esos mensajes en una tabla o archivo log, y esto sería posible gracias a haber separado la parte negocio en una clase independiente del form que se puede instanciar sin necesidad del form.



f) La parte que falta: Los Datos


En la programación tradicional de FoxPro, la Interfaz y el Negocio están mezclados, y los Datos no iban a ser la excepción, ya que cualquier consulta de datos se suele hacer en la misma Interfaz, similar a esta forma:

Form1.cmdObtenerArticulos.Click

*-- Recupero los datos
SELECT * FROM ARTICULOS ;
   WHERE CodArt = THISFORM.txtCodArt.Value ;
   INTO CURSOR c_Arts

*-- Asigno los datos
THISFORM.txtValor.Value = c_Arts.Valor
THISFORM.txtDescrip.Value = c_Arts.Descrip

*-- Proceso los datos
(código que hace varias cosas con los datos recuperados)


Cuando se separan responsabilidades y se trabaja con capas, nunca se debe hacer un acceso a datos (Select, Update, Insert, etc) directamente desde la Interfaz, sino que se debe hacer desde una clase externa para acceso a datos, por ejemplo así:

*--  ARTICULO_DB.PRG 
Define Class cl_articulo As Custom

  PROCEDURE DevolverDatosArticulo
    LPARAMETERS tcCodArt, taDatosArt
    USE ARTICULOS AGAIN SHARED NOUPDATE ALIAS _ART IN 0
    SELECT * FROM _ART ;
       WHERE CodArt = tcCodArt ;
       INTO ARRAY taDatosArt
    USE IN (SELECT("_ART"))
    RETURN _TALLY
  ENDPROC

EndDefine

Nota: No incluí control de errores ni apertura/cierre de tablas para simplificar.


Y esta clase de datos es usada por el negocio, de esta forma:

*--  ARTICULO_BUS.PRG 
Define Class cl_articulo As Custom
   c_CodArt = ""
   n_Valor  = 0
   o_DB     = NULL

   PROCEDURE ProcesarDatos
      LOCAL laDatosArt(1,1)
      THIS.o_DB = NEWOBJECT( "cl_articulo", "ARTICULO_DB.PRG" )
      THIS.o_DB.DevolverDatosArticulo( @laDatosArt )
      THIS.c_CodArt = laDatosArt(1,1)
      THIS.c_Valor  = laDatosArt(1,2)


      *-- Proceso los datos
      (código que hace varias cosas con los datos recuperados)
   ENDPROC

EndDefine


El código original del form, solo haría esta llamada:

Form1.cmdObtenerArticulos.Click
THISFORM.oBus.ProcesarDatos()

Nota: Es aquí, en la Interfaz, donde deben mostrarse los errores de los métodos invocados.



[Nuevo. 03/06/2015>>]

Posibles mejoras al modelo propuesto


El modelo propuesto es una versión muy simplificada de una arquitectura en capas, y obviamente hay mucho espacio para la mejora.

A continuación dejo algunas ideas de una estructura de aplicación basada en un objeto público (oApp) que contiene la misma clase de artículos de antes, pero dentro de una estructura más versátil. Dentro de oApp se podrá tener a las demás clases de negocio, para ser accedidas como oApp.oArticulo.oDatos u oApp.oCliente.oDatos, y para poder realizar tareas como oApp.oArticulo.ListarStock() u oApp.oCliente.DevolverSaldo()

En el programa principal (ej: Main.prg), es donde realizamos la configuración inicial de la aplicación (comandos SET, PATH, cambio de directorio, etc) y donde instanciamos el objeto oApp:


*-- MAIN.PRG
PUBLIC oApp
oApp = NewObject("cl_app_bus", "APP_BUS.PRG")
oApp.CargarNegocio()


Y podemos definir la clase cl_app de esta forma:

*-- APP_BUS.PRG
Define Class cl_app_bus AS Custom
  oArticulo = Null

  PROCEDURE cargarNegocio
    THIS.oArticulo = NewObject("cl_articulo_bus", "articulo_bus.prg")
  ENDPROC
EndDefine


En vez de usar directamente propiedades del objeto oBus para los datos, sería mejor usar una propiedad oDatos inicializada a NULL y luego guardar en ella un objeto de tipo Empty con las propiedades que se correspondan con los campos a tratar, de forma uqe luego el ControlSource de los controles del form apunten a ella, de la forma: oApp.oArticulo.oDatos.cod_Art

Para crear este objeto, se podría hacer un método de la clase de acceso a datos que lo devuelva, junto a otros métodos para su tratamiento de guardado y recuperación, por ejemplo:



*-- ARTICULO_DB.PRG
DEFINE CLASS cl_articulo_db AS Custom

  PROCEDURE DevolverDatosArticulo
    LPARAMETERS tcCodArt, taDatosArt
    LOCAL lnSelect
    lnSelect = SELECT()
    USE ARTICULOS AGAIN SHARED NOUPDATE ALIAS _ART IN 0
    SELECT * FROM _ART ;
       WHERE CodArt = tcCodArt ;
       INTO ARRAY taDatosArt
    USE IN (SELECT("_ART"))
    SELECT (lnSelect)
    RETURN _TALLY
  ENDPROC

  PROCEDURE devolverObjetoDatosVacio
    LOCAL loReg
    loReg = CreateObject("Empty")
    AddProperty(loReg, "c_CodArt", "")
    AddProperty(loReg, "n_Valor", 0)
    RETURN loReg
  ENDPROC

  PROCEDURE guardarDatos(toDatos)
    LOCAL lnSelect
    lnSelect = SELECT()
    SELECT 0
    USE ARTICULOS SHARED AGAIN ALIAS _ART ORDER c_CodArt
    IF SEEK(toDatos.c_CodArt) THEN
      GATHER MEMO NAME toDatos
    ELSE
      INSERT INTO _ART FROM NAME toDatos
    ENDIF
    USE IN (SELECT("_ART"))
    SELECT (lnSelect)
  ENDPROC

  PROCEDURE recuperarDatos(tcCodArt)
    LOCAL loDatos, lnSelect
    lnSelect = SELECT()
    loDatos = NULL
    SELECT 0
    USE ARTICULOS SHARED AGAIN NOUPDATE ALIAS _ART ORDER c_CodArt
    IF SEEK(toDatos.c_CodArt) THEN
      SCATTER MEMO NAME loDatos
    ENDIF
    USE IN (SELECT("_ART"))
    SELECT (lnSelect)
  ENDPROC

ENDDEFINE




Pero como no se puede acceder directamente a la clase de acceso a datos, sino por medio de la clase de negocio, también agregamos los métodos en la clase de negocio de Articulos:


*--  ARTICULO_BUS.PRG 
Define Class cl_articulo_bus As Custom
  o_DB    = Null
  oDatos  = Null

  PROCEDURE Init
    THIS.o_DB = NEWOBJECT( "cl_articulo_db", "ARTICULO_DB.PRG" )
    THIS.cargarObjetoDatosVacio() && objeto inicial vacío
  ENDPROC

  PROCEDURE devolverObjetoDatosVacio
     RETURN THIS.o_DB.devolverObjetoDatosVacio()
  ENDPROC

  PROCEDURE cargarObjetoDatosVacio
     THIS.oDatos = THIS.devolverObjetoDatosVacio()
  ENDPROC

  PROCEDURE guardarDatos
     THIS.o_DB.guardarDatos(THIS.oDatos)
  ENDPROC

  PROCEDURE recuperarDatos(tcCodArt)
     THIS.oDatos = THIS.o_DB.recuperarDatos(tcCodArt)
  ENDPROC

  PROCEDURE DevolverDatosArticulo
    LPARAMETERS tcCodArt, taDatosArt
    RETURN THIS.o_DB.devolverDatosArticulo(tcCodArt, taDatosArt)
  ENDPROC

EndDefine


Como se ve, la arquitectura de objetos que elijan les permite poder relacionarlos y usarlos de forma autodocumentada, si se diseña bien.

Hay muchas formas de estructurar los objetos de negocio y de armar la estructura de una aplicación, y lo anterior solo pretende dar algunas ideas de algunas posibilidades.

Algo importante a recordar es no encasillarse en una única solución, si la solución que se usa no se puede aplicar el tipo de problema que se quiere resolver, simplemente se cambia la solución.
[<<]



[Nuevo. 04/06/2015>>]

¿Y el control de errores?


No lo puse en los ejemplos para mantenerlos lo más simples y entendibles posible, además del espacio que ocupa poner más código, pero vamos a dedicarle un apartado a esto, que es fundamental en toda aplicación, y que debe hacerse mientras se programa, nunca al final.

Algunos usarán el antiguo método del ON ERROR, porque es cómodo, se escribe una sola vez y sirve para toda la aplicación, lo que lo hace tentador, pero la realidad es que ese método no es suficiente cuando se quiere controlar la recolección de basura o ciertas condiciones en un sistema, ya que deja muy pocas alternativas de gestión y salida, y es que si no se liberan bien las referencias de objeto, se pueden quedar enganchadas (si es se vinculan de forma cruzada) y esto puede causar que no se pueda salir de la aplicación.

Hay una mezcla de pereza y miedo a controlar los errores, y muchas veces se peca de demasiado optimismo pensando de que todo es correcto y está todo pensado, pero los imprevistos existen, y los errores le ocurren incluso a los mejores.

La mejor estrategia, es contar con los errores como una parte normal del desarrollo y usarlos a nuestro favor, para generar información de diagnóstico que nos permitan identificarlos y corregirlos lo antes posible.

¿Cuántas veces ven en los foros a personas que piden ayuda por un error que le da su aplicación y que no saben cómo o dónde buscarlo? Eso pasa porque no se gestionan los errores. En el mejor caso algunos ponen una rutina genérica y en el peor no ponen nada y dejan que FoxPro muestre el conocido mensaje para "Continuar, Cancelar, Reintentar", lo que es bochornoso y muy peligroso también, ya que el usuario puede elegir algo que no debe poder elegir.

En nuestro caso vamos a ver un ejemplo aplicado al método DevolverDatosArticulo(), y que servirá de ejemplo para los demás.

*-- ARTICULO_DB.PRG
<resto del código excluido por comodidad>

  PROCEDURE DevolverDatosArticulo
    LPARAMETERS tcCodArt, taDatosArt

    TRY
      LOCAL loEx as Exception, lnSelect
      lnSelect = SELECT()
      USE ARTICULOS AGAIN SHARED NOUPDATE ALIAS _ART IN 0
      SELECT * FROM _ART ;
        WHERE CodArt = tcCodArt ;
        INTO ARRAY taDatosArt

    CATCH TO loEx
      IF EMPTY(loEx.UserValue) AND _VFP.StartMode=0 THEN
        SET STEP ON
      ENDIF 

      loEx.UserValue = loEx.UserValue + "tcCodArt=" ;
        + TRANSFORM(tcCodArt) + CHR(13) + CHR(13)

      THROW

    FINALLY
      USE IN (SELECT("_ART")) 
      SELECT (lnSelect)
    ENDTRY

    RETURN _TALLY
  ENDPROC

<resto del código excluido por comodidad>


El código anterior sería la implementación completa de la funcionalidad con control de errores, en gris está el código previo para que se pueda contrastar con el código agregado.

Como este método realiza tratamiento de datos, al inicio se guarda el área de trabajo en la que se estaba, luego se abre la tabla en modo compartido y de sólo-lectura con un alias temporal (_ART) y se hace la consulta.

Si todo va bien, se ejecuta el FINALLY donde se cierra el alias temporal y se restaura el área de trabajo original.

Ahora, si hay un error, viene la parte interesante:

- Se verifica si en UserValue hay algo (inicialmente está vacío) y si estamos en modo Desarrollo (StartMode=0), en cuyo caso se abre la ventana del depurador para que podamos ver in-situ el origen del problema.

- Luego se recoge el valor del parámetro tcCodArt y se guarda en la propiedad UserValue del objeto de errores, lo cuál es muy útil, porque los valores que pongamos irán dentro de este objeto de tipo Exception hasta su destino final, donde se mostrará si es un proceso interactivo con el usuario) o se logueará al disco (si es un proceso desatendido). Esto nos dará la pista de qué artículo se estaba consultando cuando ocurrió el error. Normalmente aquí se suelen poner los valores relevantes y específicos del método que nos puedan dar pistas claras del problema o que nos ayuden a poder reproducirlo (por eso lo de "diagnóstico" que comenté antes)

- A continuación relanzamos el error al nivel superior, que normalmente será el método de negocio que haya llamado a este, o el anterior si es que el método de negocio no tiene control de errores.

- Finalmente se ejecuta el Finally, que se ejecuta cuando se relanza el error como parte del funcionamiento de la estructura Try/Catch, donde se cierra la tabla temporal y se restaura el área de trabajo original.

Lo bueno del Finally es que se ejecuta siempre, tanto haya errores como no los haya, y esto permite realizar siempre la recolección de basura.


El método de negocio DevolverDatosArticulo() no tiene casi código, por lo que es casi imposible que ocurra un error aquí (salvo uno de sintaxis), y por lo tanto no tiene sentido agregarle control de errores, ya que no aporta nada al proceso:

  PROCEDURE DevolverDatosArticulo
    LPARAMETERS tcCodArt, taDatosArt
    RETURN THIS.o_DB.devolverDatosArticulo(tcCodArt, taDatosArt)
  ENDPROC



Finalmente tenemos la interfaz, que es desde donde se lanzó el proceso. Como es un método de consulta, vamos a asumir de que se lanzó desde un form de consulta con un botón cmdConsultar y un textbox txtCodArt en el que se indicó el artículo a consultar.

Queda por aclarar un pequeño punto, que es dónde se guarda el dato ingresado por el usuario en txtCodArt. Como ya comenté al inicio, no es buena idea tomar los datos directamente del Value de los controles, por lo que vamos a crear un objeto de datos nada más que para este form, y vamos a usarlo como "THISFORM.oDatos.c_CodArt" en el ControlSource del control txtCodArt:


DEFINE CLASS cl_form AS Form
  oDatos = NULL
  BindControls = .F.

  <resto del código excluido por comodidad>

  PROCEDURE Init
    THIS.oDatos = CREATEOBJECT("Empty")
    AddProperty( THIS.oDatos, "c_CodArt", "" )
    BindControls = .T.
  ENDPROC

  PROCEDURE cmdConsultar.Click
    LOCAL laDatosArt, loEx as Exception, lcMsg
    STORE "" TO lcMsg

    TRY
      oApp.oArticulo.devolverDatosArticulo( ;
        THISFORM.oDatos.c_cCodArt, @laDatosArt)
      ...hacer algo con laDatosArt

    CATCH TO loEx
      TEXT TO lcMsg ADDITIVE TEXTMERGE NOSHOW FLAGS 1+2 PRETEXT 1+2
        Error <<loEx.ErrorNo>>, <<loEx.Message>>
        Procedure <<loEx.Procedure>>, line <<loEx.LineNo>>
        Details <<loEx.Details>
        UserValue <<loEx.UserValue>>
      ENDTEXT

      MESSAGEBOX( lcMsg )

    FINALLY
      * Recolección de basura
    ENDTRY
  ENDPROC

ENDDEFINE


Nota: Esta definición de form está incompleta y solo incluí las partes importantes, que se pueden agregar o configurar en un form real o por código, similar a este.

Veamos, por partes, qué sucede aquí:

- Primero asumimos que txtCodArt.ControlSource = "THISFORM.oDatos.c_CodArt" y que la propiedad BindControls del form está en .F. en diseño (importante para que no de error por objeto oDatos inexistente)

- En el Init se crea el objeto oDatos del form y se le agrega la propiedad c_CodArt que se usará para la consulta, y como última instrucción se habilita el bindeo del ControlSource con BindControls = .T., cosa que cuando se carga el form ya está todo lo necesario inicializado

- Cuando se ingresa el código de artículo en txtCodArt, este se guarda en THISFORM.oDatos.c_CodArt y al pulsar el botón cmdConsultar se invoca a la consulta de Artículos del negocio, pasándole el código del artículo y recibiendo un array con los datos

- Si va todo bien, con el array luego se hace lo que se necesite, pero si ocurre un error en algún punto de todo el proceso, el mismo se irá relanzando desde donde ocurra hasta llegar aquí a la interfaz (lo que se conoce como "propagación del error"), donde finalmente se arma un mensaje para visualizar con los datos obtenidos y se muestra.

Como se ve, no es nada difícil manejar la propagación del error, ya que desde fuera de la interfaz, lo único que hay que hacer es propagarlo, y solo en la interfaz hay que mostrarlo. También sería conveniente guardarlo en un archivo LOG en el momento que se produce.


Estas son algunas preguntas comunes que surgen sobre este tema (FAQ):

¿Cómo se puede identificar al error original de uno relanzado?

  Cuando se captura un error y se relanza, en todos los puntos intermedios en los que se relance, tendrá la misma información, pero si tomamos como costumbre siempre poner algún dato en la propiedad UserValue, aunque sea "." si no hay nada útil que agregar, podemos saber si se estamos evaluando el error original solo evaluando si UserValue está vacío, que lo estará solamente la primera vez.

¿Cómo se puede distinguir a un error de verdad de un error de usuario lanzado con ERROR "mensaje"?

  Los errores de usuario lanzados con ERROR "mensaje" tienen el código 1098, los errores de usuario lanzados con THROW "mensaje" tienen el código 2071.
[<<]



Ventajas de separar en capas


Como hemos visto, hacer esta separación entre Interfaz (los controles, el form, lo "visual"), Negocio (validaciones, cálculos, procesos) y Datos (consultas, eliminación, actualizaciones) permite poder hacer varias cosas que estando metidas en un form son difíciles o imposibles, por ejemplo:

  • Cuando un método necesita pasar datos a otro, puede pasarle directamente el objeto de negocio, dependiendo de dónde esté definido cada uno, y así evitar pasar datos individuales
  • Permite reutilizar las validaciones en otros forms u otras situaciones o procesos
  • Permite encapsular las validaciones, lo que hace que sean más fáciles de mantener
  • Permite encapsular los accesos a datos, lo que hace que sean más fáciles de mantener también
  • Hace que el código sea más específico y por lo tanto más reducido y fácil de entender
  • Permite usar Testeo Unitario automatizado, por ejemplo, con FoxUnit, lo que puede ahorrar muchas horas o incluso días de pruebas manuales y de regresión
  • La encapsulación favorece la reusabilidad del código, y más todavía cuanto mejor se parametrice
  • Permite poder trabajar en cada capa de forma independiente, hasta el punto de que cada capa la puede hacer un desarrollador distinto si se quiere, y reducir tiempos de desarrollo y pruebas
  • Incluso aunque se trabaje en solitario, las pruebas también se hacen más simples
  • La búsqueda de errores se simplifica, ya que al tener bloques de código más reducidos y especificos, se recorren de forma más rápida
  • Al separar la Interfaz de las validaciones, los cálculos, las consultas y demás, la Interfaz queda con lo mínimo y necesario para la navegación, lo que permitiría poder sustituirla por un nuevo diseño de Interfaz sin perjudicar ni perder ninguno de los procesos, y obviamente sin tener que copiar su código (salvo las llamadas a los procesos)


¿Cuántas capas existen?


Se llama N capas porque la cantidad de ellas que se usen dependerá del diseño y arquitectura que se quiera. Es muy importante no encasillarse en "3", ya que fácilmente pueder ser varias más, y para evitar ese encasillamiento es que también se habla de "separación de responsabilidades".

Tomemos por ejemplo una conexión a web-services: En este caso podríamos hablar de una nueva capa o responsabilidad que se encargue de todas las comunicaciones de este tipo.

Dentro de la capa de comunicaciones, se podrían tener sub-capas más específicas, como la antes comentada de web-services, o una de envío de mensajes o SMSs, etc.

Otro ejemplo: La impresión se puede considerar como otra capa, donde todo lo que tenga que ver con eso esté encapsulado en una o más librerías específicas, que pueden ser utilizadas desde donde sea necesario.

Como se podrán ir dando cuenta, a medida que vayan hilando más fino podrán ver que subyace la idea de "servicios" específicos con APIs que pueden ser reutilizados desde distintos puntos del sistema, incluso al extremo de poder ser tan reutilizables que sirvan para otros sistemas. Esa misma idea es la que hay detrás de "servicios" tan útiles y específicos como el portapapeles (clipboard), el servicio de Impresión, el servicio de mensajes del sistema, los servicios de red, etc.


Aunque no lleguen a ese punto, lo importante es que puedan ver el potencial de hacer componentes o servicios tan específicos que puedan ser utilizados desde cualquier programa, incluso no FoxPro.



Notas finales


La idea de este artículo es poder ver las diferencias entre una y otra forma de trabajar, la forma antigua ya tradicional de FoxPro donde se hace todo junto, y la otra que no es nada nueva y donde los resultados pueden ser mejor aprovechados bajo la filosofía de separar en componentes y responsabilidades, llevando la encapsulación del código a los niveles necesarios para permitir la reusabilidad y favorecer la mejor claridad del código.

Comparando los ejemplos de la programación tradicional en FoxPro del "todo junto" con los ejemplos separados en sus distintos componentes (visual, negocio y datos), creo que puede comprobarse que aunque lo segundo requiera algo más de código, realmente cada parte es muy específica y simple de entender.

Intenté que los ejemplos sean sencillos y no he incluido el control de errores para mantener los ejemplos cortos y fáciles de leer y de entender, ya que se asume que el control de errores forma parte integral de la programación y no algo separado.

De la misma forma, el Testing Unitario automatizado es una herramienta muy importante a la que se le puede sacar todavía mayor provecho cuando se usa esta metodología de trabajo, y que tampoco debiera ser considerada como algo aparte.

Si se habitúan a pensar de esta forma y a trabajar con esta metodología, de pronto se encontrarán con que comenzarán a tener varias posibilidades, soluciones e ideas que hasta ahora no habían podido ver.



Hasta la próxima! :D

Artículos Relacionados:
Unit Testing: Qué es y cómo se usa en VFP 9
Desmitificando el Control de Errores con Try/Catch
Guía de Buenas Prácticas de programación y recomendaciones

viernes, octubre 10, 2014

PlasticSCM: Qué es el Cherry Pick y cómo se usa

Por: Fernando D. Bozzo

El Cherry Pick, que se traduce como "selección de cerezas", es una operación que permite seleccionar solamente los cambios de una rama o changeset, sin incorporar todo el resto de archivos heredados.

En otras palabras, si un changeset o rama tiene 2 archivos nuevos, pero ya existían 10 archivos previamente, el cherry pick permite seleccionar los 2 archivos nuevos, y no todos los archivos que ya existían, como haría el merge.



Ejemplo 1 - Cherry pick de un changeset



Supongamos que tenemos la rama-1 de la imagen, con 3 changesets (3, 4 y 5):



Y que en cada changeset hemos ido agregando un archivo de texto, archivo1.txt en el changeset 3, archivo2.txt en el changeset 4 y archivo3.txt en el changeset 5, como vemos en la siguiente lista (columna derecha):



Si quisiéramos pasar el contenido del changeset 5 (donde se agregó el archivo3.txt) de la rama-1 a la rama-2, que ahora está vacía, normalmente seleccionaríamos el cs:5 y luego "merge desde este changeset" en el menú, pero en este caso el merge mostraría lo siguiente:



Como vemos, nos trae no solo lo del changeset 5 (cs:5), sino todos los archivos heredados de las operaciones anteriores.

Para resolver este caso, lo que podemos hacer es un Cherry Pick para seleccionar exclusivamente el contenido del changeset elegido, pero sin incluir lo heredado, para lo que seleccionando el cs:5 elegimos en el menú del click-derecho la opción "Cherry pick de este changeset", lo que mostrará lo siguiente:



Con esto ya tenemos los cambios de ese changeset particular y el explorador de ramas mostrará el cherry pick con una línea violeta:





Ejemplo 2 - Cherry pick de una rama


Lo mismo que pasó antes con los archivos de un changeset, se aplica a los archivos de una rama (que es la suma de todos sus changesets), por lo que al realizar un Cherry pick de una rama, estaremos pasando al destino todos los archivos modificados en esa rama, indistintamente de si se modificó en el primer o último changeset, pero sin heredar todos los archivos anteriores a la misma.

Para hacer un cherry pick de una rama, el intervalo se selecciona eligiendo el changeset anterior a la rama y el último de la rama (intervalo abierto-cerrado):





Ejemplo 3 - Cherry pick para restaurar un changeset quitado con un merge sustractivo


Supongamos que hicimos un merge sustractivo para quitar de una rama los cambios hechos en el mismo, porque había un error que requerirá tiempo para corregir, pero se quiere poder seguir trabajando con el resto de cambios.

Usando el primer ejemplo, si quisiéramos quitar el changeset 4 que contenía el archivo2.txt, haríamos click-derecho en ese changeset, seleccionamos "Merge avanzado" y luego "Merge sustractivo del changeset...", lo que mostraría lo siguiente:




Y una vez hecho el checkin, se reflejaría en el diagrama de ramas con una línea roja:



Si exploráramos el repositorio en el nuevo changeset, comprobaríamos que el archivo2.txt se quitó:



Si quisiéramos volver a incorporar los cambios de ese changeset, entonces tendríamos que seleccionar "Cherry pick de este changeset", lo que mostraría lo siguiente:




¿Pero por qué no muestra el archivo de antes? Porque Plastic lleva la cuenta de las operciones que hemos realizado antes, y como ya le hemos indicado que no queríamos ese archivo y tampoco hay nada nuevo que agregar, entonces no muestra nada para hacer.

Para forzar a Plastic a que incluya los cambios de ese changeset, debemos indicarle que no tenga en cuenta las operaciones anteriores, lo que se hace eligiendo el botón inferior "Opciones de merge..." y luego marcando la opción "Ignorar merge tracking":




Y ahora sí, mostrará el archivo que queríamos recuperar:




Al finalizar el checkin, en el diagrama de ramas veremos una línea violeta:



Y explorando los archivos en ese changeset, ya veremos todos los archivos nuevamente:





Ejemplo 4 - Cherry pick para restaurar una rama quitada con un merge sustractivo


Este caso es como el anterior, pero cambia la forma de seleccionar el intervalo (abierto-cerrado).

Así como para hacer un merge sustractivo de una rama se debe seleccionar el changeset anterior a la rama (1) y el último changeset de la rama (2) (intervalo rojo difuso):




Para hacer un cherry pick de una rama, el intervalo se selecciona de igual forma (intervalo violeta difuso):




Y al igual que en el Ejemplo 3, cuando el cherry pick se hace para restaurar algo que anteriormente fue quitado total o parcialmente, no hay que olvidar elegir "Opciones de merge.." y marcar la opción "Ignorar merge tracking"




Resumen



Como hemos visto, el Cherry Pick es muy útil en varias situaciones, permitiendo realizar una selección puntual para una operación precisa.

En varios casos puede reemplazar al Merge normal, y en situaciones donde el merge sea demasiado complicado y muestre los bloques de código a mezclar muy "desordenados", vale la pena probar con el Cherry Pick, cuyo algoritmo de merge puede mostrar esos mismo bloques más fáciles de mezclar.

La única particularidad que se debe recordar, es que si hubo una merge sustractivo anterior, se debe marcar la opción "Ignorar merge tracking" en las opciones del merge, y así evitaremos no solamente la situación de no ver ningún archivo, sino que además permitirá resolver un merge que requiera intervención manual, de una forma más óptima y clara.


Hasta el próximo artículo! :D

miércoles, octubre 08, 2014

Nueva versión v2.5.2 de las herramientas Visual FoxPro 9 para PlasticSCM (Incluye FoxBin2Prg.exe v1.19.36)


Estas herramientas son un grupo de scripts vbs y programas Visual FoxPro 9 que se configuran dentro de PlasticSCM para poder invocar a FoxBin2Prg (incluye solo el EXE) desde dentro de la interfaz de Plastic.


Está liberada la versión v2.5.2 de las herramientas Visual FoxPro 9 para PlasticSCM, con los siguientes cambios:





El README.txt explica como se configura en Inglés y Español, y también está explicado en esta nota: Cómo configurar las Herramientas de VFP 9 para Plastic


Nota: Los fuentes del proyecto FoxBin2Prg y el historial de ambios, están en CodePlex, en este link.


Como actualizar las existentes:
Con descargarlas y reemplazar los archivos en el sitio que los hayan puesto antes es suficiente.


Link de descarga:
https://github.com/fdbozzo/foxpro_plastic_diff_merge


Saludos!

Nueva versión v1.19.36 de FoxBin2Prg (bug fix, mejora)

FoxBin2Prg es un programa pensado para ser utilizado con herramientas SCM (Administradores de Control de Código Fuente, como VSS, CVS, SVN) y herramientas DVCS (como Git, Mercurial, Plastic, and others), o como programa independiente, para hacer operaciones de Diff (ver diferencias) y Merge (mezclar cambios), que pretende sustituir a SccText(X) y TwoFox y mejorar sus funcionalidades, generando versiones de texto estilo-PRG que permiten recrear el binario original.

Está liberada la versión v1.19.36 de FoxBin2Prg con los siguientes cambios:

  • Bug Fix mnx: Al generar el mn2 el identificador queda vacío (bug introducido en v1.19.35)
  • Mejora: Nuevo script "VFP9_FoxBin2Prg.vbs" para el menú "SendTo" (Enviar A), que reemplaza a FoxBin2Prg.exe en este menú y agrega un mensaje de estado del proceso.


Como actualizar el FoxBin2Prg existente:Con descargar el zip y reemplazar los archivos en el sitio que los hayan puesto antes es suficiente.


Link  de descarga:
https://vfpx.codeplex.com/releases/view/116407


 Saludos!

domingo, octubre 05, 2014

Nueva versión v2.5.1 de las herramientas Visual FoxPro 9 para PlasticSCM (Incluye FoxBin2Prg.exe v1.19.35)


Estas herramientas son un grupo de scripts vbs y programas Visual FoxPro 9 que se configuran dentro de PlasticSCM para poder invocar a FoxBin2Prg (incluye solo el EXE) desde dentro de la interfaz de Plastic.


Está liberada la versión v2.5.1 de las herramientas Visual FoxPro 9 para PlasticSCM, con los siguientes cambios:





El README.txt explica como se configura en Inglés y Español, y también está explicado en esta nota: Cómo configurar las Herramientas de VFP 9 para Plastic


Nota: Los fuentes del proyecto FoxBin2Prg y el historial de ambios, están en CodePlex, en este link.


Como actualizar las existentes:
Con descargarlas y reemplazar los archivos en el sitio que los hayan puesto antes es suficiente.


Link de descarga:
https://github.com/fdbozzo/foxpro_plastic_diff_merge


Saludos!

Nueva versión v1.19.35 de FoxBin2Prg (mejora)

FoxBin2Prg es un programa pensado para ser utilizado con herramientas SCM (Administradores de Control de Código Fuente, como VSS, CVS, SVN) y herramientas DVCS (como Git, Mercurial, Plastic, and others), o como programa independiente, para hacer operaciones de Diff (ver diferencias) y Merge (mezclar cambios), que pretende sustituir a SccText(X) y TwoFox y mejorar sus funcionalidades, generando versiones de texto estilo-PRG que permiten recrear el binario original.

Está liberada la versión v1.19.35 de FoxBin2Prg con los siguientes cambios:

  • Mejora: Generar siempre el mismo Timestamp y UniqueID para los binarios minimizaría los cambios al regenerarlos (Marcio Gomez G.). Hasta ahora, al regenerar los binarios se regeneraban también los campos Timestamp con la fecha/hora del momento de generación y UniqueID con el SYS(2015), lo que forzaba diferencias aunque no las hubiera. Con la mejora propuesta, al Timestamp se le pone un valor fijo y al UniqueID se le pone un consecutivo, con lo que si no hay cambios, tampoco variarán estos datos (se generarán siempre igual), por lo que los binarios serán iguales y no se mostrarán como si tuvieran cambios.


Como actualizar el FoxBin2Prg existente:Con descargar el zip y reemplazar los archivos en el sitio que los hayan puesto antes es suficiente.


Link  de descarga:
https://vfpx.codeplex.com/releases/view/116407


 Saludos!

domingo, septiembre 28, 2014

VFP: Guía de Buenas Prácticas de Programación y recomendaciones

Por: Fernando D. Bozzo

Hace poco publiqué unas Reglas Mínimas de Buenas Prácticas de Programación, orientadas principalmente a las prácticas con el control de código fuente que estábamos haciendo.

En esta ocasión voy a tomar parte de lo escrito allí y a extenderme más sobre esas y algunas otras, de forma de tener una Guía más completa respecto de la programación en general con algunas cosas específicas de FoxPro y en algunos casos con el por qué o en qué beneficia hacerlo, ya que personalmente considero muy importante qué es lo que motiva a recomendar hacer las cosas de una forma determinada.



¿Qué son y para qué sirven las buenas prácticas?


Las buenas prácticas no existen desde siempre, sino que se fueron desarrollando como resultado de la experiencia común de muchos desarrolladores y empresas dedicadas al desarrollo de software, como Microsoft y otras.

En esas experiencias se han pasado muchas veces por situaciones conflictivas, que luego de haber identificado fallos o problemas comunes, y luego de haber analizado los motivos que llevaron a ellos, se han ido perfeccionando ciertas prácticas que minimizan la posibilidad de los mismos.

Otra ventaja de las buenas prácticas, es que tienden a facilitar el mantenimiento del código, no solo por la persona que lo hace, sino por otros, lo que permite "normalizar" la forma de hacer las cosas, bajar los tiempos de desarrollo y maximizar la eficiencia.

Finalmente, el uso de estas normas minimiza los problemas y las diferencias a la hora de tener que mezclar el código de dos desarrollos sobre los mismos componentes (llamado merge), aumentando la posibilidad de que sea automático y no requiera intervención manual.




1. Convención de nomenclatura de variables (VFP)


Está detallada en la ayuda de FoxPro y en el MSDN de Microsoft

Resumen:
  • La primera letra define el alcance (local, privada, global)
  • La segunda letra define el tipo de dato (numérico, carácter, date, etc)
  • El nombre debe definir el concepto que representa

Ejemplos de nomenclatura:
Parámetros (t) => tcCadena, tnNumero, tdFecha, tlBoolean, taArray, toObjeto
Variables Locales (l) => lcCadena, lnNumero, ldFecha, llBoolean, laArray, loObjeto
Variables Privadas (p) => pcCadena, pnNumero, pdFecha, plBoolean, paArray, poObjeto
Variables Publicas (g) => gcCadena, gnNumero, gdFecha, glBoolean, gaArray, goObjeto

Ejemplos de buen uso conceptual:
ldFechaNacimiento
glUsuarioActivo
llActivateEjecutado


Beneficios:
  • Permite distinguir rápidamente los distintos alcances y tipos de variables y propiedades, por lo que pueden convivir variables o propiedades del mismo nombre y distinto alcance o tipo (ej: gdFecha y ldFecha)
  • Ayudan, en parte, a ver si un método está bien encapsulado o si requiere mayor parametrización. Como regla general, conviene usar siempre variables locales para mantener la encapsulación de la funcionalidad
  • Impide que se confunda una variable con un nombre de campo (lcClave y clave)
  • Ayuda a entender mejor el código y lo que hace



2. Convensión de nomenclatura de objetos (VFP)


Está detallada en la ayuda de FoxPro y en el MSDN de Microsoft

Resumen:
  • Las primeras 3 letras definen el tipo de objeto
  • El nombre debe definir funcionalmente para qué sirve

Ejemplos de nomenclatura:
custom => cus_nombre
label => lbl_nombre
form => frm_nombre
checkbox => chk_nombre
editbox => edt_nombre
listbox => lst_nombre
spinner => spn_nombre
pageFrame => pgf_Nombre
page => pag_Nombre
commandButton => cmd_nombre

Ejemplos de buen uso conceptual:
txt_FechaDeNacimiento
cmd_CerrarFormulario
pag_ConfiguracionesDeUsuario
frm_PermisosPorUsuarioYGrupo


Beneficios:
  • Permite distinguir rápidamente los distintos tipos de objetos
  • Ayuda a entender mejor el código y lo que hace



3. Checkin y checkout de componentes (SCM/VFP)


Recomendación:
Antes de hacer un checkin o un checkout de componentes, es conveniente limpiar el entorno para evitar problemas:

CLEAR ALL
CLOSE ALL

o incluso cerrar la sesión de VFP si se usa para ejecutar, y luego ya se puede hacer el checkin o checkout.


Beneficios:
  • Evitar errores por "archivo en uso"
  • Evitar hacer checkin de partes del componente (proteger SCX sin su SCT, etc) si el SCM no usa transacciones atómicas (todo o nada)





4. Sesiones de FoxPro


Recomendación:
  • Evitar cambiar el directorio de una sesión de FoxPro que sea usada para modificar componentes, o sea, no usar CD <directorio> o SET DEFAULT TO <directorio> si se modifican programas con MODIFY o se ejecutan con DO.
  • Para distintos sistemas o programas en distintos directorios, es mejor usar sesiones independientes de VFP (o sea, abrir un VFP para cada sistema)


Beneficios:
  • FoxPro suele cachear los programas y clases en la memoria, y al modificar en otra ubicación se podrían guardar rutas inválidas o apuntando a otro directorio, sobre todo en las clases y forms
  • Usando sesiones de FoxPro independientes, se puede editar en una y ejecutar en otra, y si algo sale mal durante la ejecución y hay que matar esa sesión, la edición y el proyecto se mantienen sin cambios en la otra sesión y se evita que se corrompan los archivos



5. Recepción de Parámetros en métodos


Recomendaciones:
  • Usar LPARAMETERS en vez de PARAMETERS y si hay que agregar parámetros a un método, agregarlos siempre al final, nunca en medio o al inicio de los parámetros existentes.
  • Usar PCOUNT() en vez de PARAMETERS()

Ejemplo agregando nuevo parámetro "tnCosto":
LPARAMETERS tcNombre, tnEdad, tnCosto
lnParams = PCOUNT()


Beneficios:
  • LPARAMETERS crea parámetros Locales, o sea que se limpian automáticamente al terminar el método y los métodos que se llamen no verán las variables creadas, mientras que PARAMETERS los crea Privados y seguirán siendo visibles al llamar a otros métodos
  • Creando los parámetros nuevos al final, se garantiza la compatibilidad de las llamadas existentes a este método sin necesidad de pruebas de regresión, porque de otra forma habría que cambiar todas las llamadas y volver a probar todo lo existente para verificar que no falle (pruebas de regresión)
  • PCOUNT() lleva la cuenta de los parámetros del método actual, mientras que PARAMETERS() lleva la cuenta de los últimos parámetros recibidos en cuelquier método, incluyendo métodos externos al actual, lo que puede producir errores de evaluación



6. Uso de referencias de objeto


Recomendaciones:
  • Definir siempre que sea posible las variables de objeto indicando la clase y librería de pertenencia
  • Usar nombres significativos, que indiquen claramente para qué sirven
  • Antes de liberar la referencia del objeto, asignarle NULL
  • Liberar siempre las variables de objeto con RELEASE, y no confiar en la limpieza automática de Fox, porque a veces queda la referencia en memoria
  • Usar variables de objeto en lugar de rutas de objetos tipo obj1.obj2.obj3.objN
  • Si se deben usar muchas referencias de objetos en un bucle, usar WITH objeto .. ENDWITH
  • Aunque no está documentado, en los parámetros definidos con "AS clase" para aprovechar Intellisense, no es conveniente referenciar clases de librerías o programas externas, sino solo clases nativas, ya que con las clases externas se puede provocar un error C0000005 fatal. Para referencias clases externas es mejor usar el truco del IF .F. descripto en el ejemplo de debajo, donde nunca se ejecutará su contenido, pero servirá para usar Intellisense.


Ejemplo 1: Referencia de objeto por parámetro
LPARAMETERS toEntorno, toException as Exception, tnCod as Integer
IF .F. && Truco para no ejecutar, pero poder definir Intellisense
   LOCAL toEntorno AS "Entorno" OF "LIB_ENTORNO.PRG"
ENDIF
toEntorno.EspacioMinimoDeDisco = 100 && MB



Ejemplo 2: Uso de With..EndWith de forma óptima
WITH THISFORM.pgfConfig.pagEntrada.txtContador
   FOR I = 1 TO 1000
      .VALUE =.VALUE + 1
   ENDFOR
ENDWITH



Ejemplo 3: Uso de Referencias de objeto cacheadas en variables 
LOCAL loPagEntrada AS Page
loPagEntrada = THISFORM.pgfConfig.pagEntrada
loPagEntrada.txtUsuario.VALUE = ""
loPagEntrada.txtPassword.VALUE = ""
loPagEntrada.chkDiasVigencia.VALUE = 15


Beneficios:
  • Definir las variables indicando la clase y la librería, permite usar Intellisense y acceder a la lista de miembros de la clase al poner el punto (objeto.<miembros>), lo que ahorra tiempo
  • Usar nombres significativos facilita la compresión y claridad del código, y el mantenimiento futuro
  • Asignar NULL a las referencias de objetos al terminar evita la referencias pendientes de tipo objeto, cuya acumulación puede provocar el error C0000005
  • Liberar las variables de objeto con RELEASE es más rápido y seguro que el recolector de basura de Fox, donde a veces quedan colgadas en memoria
  • Usar variables de objeto (ej: loPagEntrada) en vez de referencias de objeto largas (ej: thisform.pgfConfig.pagEntrada) permite ganar velocidad de ejecución por no requerir leer cada objeto de la secuencia, además permite mayor claridad en el código y ocupar bastante menos espacio, lo que hace que se pueda leer y entender más rápido
  • Usar With..EndWith cuando hay muchas referencias de objeto o referencias dentro de un bucle FOR o DO WHILE siempre es lo más rápido, ya que el WITH cachea el objeto



7. Return


Recomendaciones:
    • No usar RETURN en medio los métodos, sino sólo al final de los mismos, ya que si no se rompe el flujo natural del programa
    • Jamás usar RETURN dentro de un ENDWITH! Deja referencias de objeto colgadas y al rato genera el error C0000005
    • Agregar RETURN siempre al final de un método o procedimiento, para poder elegirlo durante una depuración y poder salir del método antes


    Ejemplo:
    LPARAMETERS tnValor
    lnCodError = 0 
    IF tnValor <= 0
        lnCodError = 1
    ENDIF
    RETURN lnCodError


    Beneficios:
    • Los RETURN al final del código permiten seguir el flujo del programa más fácilmente
    • Se facilita la recolección de basura (liberación de tablas y variables, etc) en un único punto
    • Es más fácil para depurar y permite saltearse todo el código e ir directo al Return para salir del método (a veces viene bien al depurar)



    8. Manejo de Errores


    El control de errores es fundamental en cualquier aplicación y no debe dejarse de lado o para el final, sino que debe formar parte del diseño. Este artículo te ayudará a entender cómo funciona el Try/Catch y cómo usarlo.

    Recomendaciones:
    • Controlar los errores en todos los puntos clave del sistema y no ignorarlos
    • No dejar que FoxPro controles los errores con Abort-Retry-Ignore, porque es peligroso dejar esa decisión al usuario, ya que normalmente "ignorará" para seguir adelante
    • Los errores se deben relanzar desde la capa de datos y negocio hacia la interfaz visual, donde se muestran
    • Nunca deben mostrarse errores en una capa que no sea la Interfaz visual


    Ejemplo:
    TRY
       lnCodError = 0 
       IF tnValor <= 0
          lnCodError = 1
       ENDIF

    CATCH TO loEx
       THROW

    FINALLY
       *-- Recolección de basura
    ENDTRY


    Beneficios:
    • Try/Catch permite el manejo estructurado de errores, y relanzarlos o tratarlos localmente
    • Finally permite recolectar la basura, tanto con o sin errores
    • Relanzar los errores de negocio o de datos permite reutilizar esas lógicas, encapsularlas y testearlas de forma más fácil y aislada
    • Al ser una estructura de control que envuelve al método, facilita el flujo del programa y permite salir de él con Exit, siempre por el final del método
    • No permite usar RETURN dentro de la estructura (genera un error), lo que ayuda a estructurar y diseñar mejor el proceso



    9. Separación en N capas y en funcionalidades (encapsulación)


    Recomendaciones:
    • Si se quiere usar la separación de 3 capas (datos, negocio e Interfaz), es importante no hacer los procesos en el form, sino en un proceso independiente que puede ser invocado desde el form, pero programado fuera de él. Quedan excluidos los procesos necesarios para gestionar la Interfaz, como rutinas de refresco y similares.
    • Con los accesos a datos lo mismo: evitar usar sentencias DML (Data Manipulation Language, típicamente SQL) como Select, Update, Insert o Replace, dentro del form, y hacerlo fuera del form en procesos separados, por ejemplo las consultas de clientes o de artículos son casos típicos. Quedan excluidos los accesos a datos como necesidad del manejo de la pantalla, como cursores con metadatos usados para gestionar la Interfaz.
    • Una misma librería no debería contener elementos de capas distintas (por ejemplo, forms, clases de negocio y clases de acceso a datos)
    • Cada capa puede estar contenida en una o más librerías, dependiendo del diseño
    • Evitar que los métodos o procedimientos hagan muchas cosas y preferir que hagan una sola cosa a la vez
    • Si un método es muy largo o tiene muchos comentarios, es muy probable que haga varias cosas, lo que dificulta su mantenimiento, y sería mejor separarlo en métodos más chicos
    • Los procesos complejos que requieren realizar varias tareas, conviene dividirlos de tal forma que haya un proceso central que solo gestione subprocesos (paso de datos entre ellos, gestión de errores, etc) mediante llamadas a los mismos, donde el subproceso es el único que implementa el código de la tarea necesaria
    • Si se tienen las típicas hiper-librerías que lo hacen todo y que de tanta funcionalidad mezclada es son difíciles de mantener, una buena técnica de encapsulación, y muy simple, es reemplazar métodos con código por llamadas a métodos de librerías externas donde se mueva y adapte ese código. De esa forma se mantiene la API de la interfaz (los nombres de los métodos) pero se cargará mucho más rápido porque ya no contendrá todo el código inicial, sino solo las llamadas.
    • Hay casos en los que es necesario rediseñar (cambiar la funcionalidad). No tener miedo de hacerlo, ya que puede ahorrar más tiempo que el que lleva mantener un código espagueti



    Ejemplo 1: Código de una funcionalidad para externalizar
    *-- Consultar artículo
    (Aquí estaría todo el código que implementa la consulta, el control de errores, etc)
     
    Ejemplo 1: Código externalizado como funcionalidad independiente
    oBus.ConsultarArticulo(lcCodArt)

    Ejemplo 2: Código de múltiples funcionalidades para externalizar
    *-- Funcionalidad 1
    (código con implementación de la funcionalidad 1)

    *-- Funcionalidad 2
    (código con implementación de la funcionalidad 2)

    *-- Funcionalidad 3
    (código con implementación de la funcionalidad 3)


     
    Ejemplo 2: Código externalizado como funcionalidades independientes
    oBus.Funcionalidad_1()

    oBus.Funcionalidad_2()

    oBus.Funcionalidad_3()  




    Beneficios:
    • La separación en capas permite el mantenimiento por separado y granular de los distintos subsistemas
    • Esta separación permite maximizar las posibilidades de reutilizar código en procesos desatendidos o para otros sistemas o relacionados al actual.
    • La separación en librerías favorece su reutilización
    • La separación en funcionalidades bien definidas que hacen una sola cosa en lo posible, no solo favorece la reutilización, sino también que facilita su testeo de forma separada al sistema mediante herramientas de Testing Unitario como FoxUnit
    • La creación de métodos "Gestores" unido a una buena nomenclatura funcional, facilita el análisis, comprensión y mantenimiento del sistema, evitando tener que entrar en cada método para saber lo que hace
    • En muchas ocasiones, el rediseño ahorra tiempo y mejora la calidad y mantenibilidad del código




    10. Desarrollo sencillo y tonto (KISS)


    Recomendaciones:
    • Desarrollar con la simplicidad en mente
    • Evitar complejidades innecesarias
    • Si algo se complica, probablemente convenga separarlo en partes más simples o usar un método gestor de procesos
    • Evitar los mega-procedimientos que hacen de todo, ya que suelen ser los más difíciles de arreglar si fallan, o de mantener, pasado un tiempo
    • Usar nomenclatura clara que todos entiendan, y no "yo solo me entiendo", porque dentro de varios meses será "ni yo mismo me entiendo!"
    • Cuando se modifica un código existente, siempre evaluar si hay algo mejorable o que pueda escribirse de forma más clara. Si se puede refactorizar, hacerlo en el momento; si se puede mejorar, pero es algo complejo o que llevará más tiempo, preparar una tarea posterior para "mantenimiento preventivo" y hacerla lo antes posible (futuro cercano, no lejano:)
    • Usar los comentarios estrictamente necesarios, pero usarlos bien. 
    • Los comentarios deben explicar brevemente la funcionalidad no-obvia a primera vista, de otro modo, sobran y mejor no ponerlos

    Ejemplo 1: Código para mejorar
    If condición1
    Else
       If condición2
       Else
          If condición3
          Else
          EndIf
       EndIf
    EndIf

    Ejemplo 1: Código mejorado
    Do Case
    Case condición1
    Case condición2 
    Case condición3
    Otherwise
    EndCase

    Ejemplo 2: Comentario obvio
    *-- Muestra un mensaje
    MESSAGEBOX( "Este es un mensaje!" )

    Ejemplo 2: Optimización
    MESSAGEBOX( "Este es un mensaje!" )


    Ejemplo 3: Comentario obvio
    *-- Si lActivateEjecutado=.F.
    IF NOT lActivateEjecutado THEN
       ...(código)

    Ejemplo 3: Comentario más útil
    *-- Si no se ejecutó el evento activate
    IF NOT lActivateEjecutado THEN
       ...(código)




    Beneficios:
    • Cuanto más simple es un método, menos código tiene y más fácil de entender y mantener es
    • Menos código implica menor probabilidad de errores
    • Los nombres de métodos y variables que indican conceptos precisos y limitados, facilitan la legibilidad del código, su mantenimiento, su refactorización y su encapsulación
    • Un código fácil de ver y mantener, implica un menor costo de mantenimiento


    11. Desarrollar, Preparar Tests, Refactorizar y ejecutar Tests


    Recomendaciones:
    • Las tareas de un ciclo de desarrollo óptimo, sin un orden específico, son:
      • Desarrollar la funcionalidad
      • Crear tests automatizados (siempre que aporte beneficios)
      • Refactorizar el código para simplificarlo lo más posible
      • Ejecutar los tests antes creados para verificar que no se haya roto nada.
    • El orden de estas tareas puede variar dependiento del paradigma de programación que se esté usando. Por ejemplo, si se usa TDD (Test Driven Development o Desarrollo Orientado por Tests), los Tests Unitarios automatizados es lo que se hace primero para generar lo que se conoce como "contrato", y luego se realiza el desarrollo para cubrir esa funcionalidad (y cumplir con el "contrato" funcional)
    • La refactorización es parte de la tarea de desarrollo y no una mejora independiente para una etapa posterior o futura.
    • Crear Tests Unitarios automatizados para cualquier funcionalidad o método donde el test aporte valor, por ejemplo, para métodos de cálculo, de evaluación de decisiones, de procesos y de cualquier funcionalidad, por pequeña que sea, cuyo mal funcionamiento pueda ser muy perjudicial para el sistema. No tiene sentido hacer Tests sobre cada método existente, porque hacerlos también conlleva un coste, y se debe buscar un equilibrio.


    Beneficios:
    • El hecho de realizar estas tareas de desarrollo (desarrollo, tests, refactorización) permite maximizar la calidad desde el principio
    • La refactorización permite optimizar, depurar, limpiar y aclarar el código que se está escribiendo sin cambiar la funcionalidad, de cara a facilitar el mantenimiento del mismo
    • Los Tests Unitarios automatizados permiten realizar comprobaciones automatizadas de procesos o funciones importantes para el sistema y además, como se mantienen en el tiempo, sirven como pruebas de regresión para las siguientes versiones, lo que implica un gran ahorro de tiempo de pruebas manuales
    • Donde personalmente comprobé que TDD ahorra tiempo, es para la solución de incidencias, donde se sabe de antemano qué respuesta se esperaba de una función o proceso. En estos casos, se hacen primero los tests para reproducir el problema o error (sale el test en rojo), luego se hace la corrección del código y finalmente se vuelve a ejecutar el test, que ya debe salir en verde, lo que también permite comprobar que el test estaba bien hecho y que hace lo que debe.



    12. Usabilidad


    La usabilidad es la facilidad de poder usar un sistema.

    Nota: Los efectos especiales que a algunos desarrolladores les gusta agregar a sus sistemas, a veces terminan haciendo que la usabilidad del mismo empeore, por lo que se debe tener cuidado en esto.

    Recomendaciones:
    • Permitir recorrer los controles con el teclado con un orden lógico y no caótico (normalmente con la tecla Tab o Enter), comenzando por arriba a la izquierda, e ir avanzando en sentido abajo-derecha
    • Minimizar el uso de líneas y cajas 3D que tan de moda estuvieron con Windows 95 (1995)
    • En el diseño de las pantallas, realizar agrupaciones y separaciones funcionales por medio de espaciado entre bloques, lo que sustituye a las líneas que antes se ponían por todos lados
    • En vez de usar cajas (shapes, lines) para agrupar controles, intentar usar una línea de separación a modo de título y espacios a izquierda, derecha y debajo que separen del resto
    • Alinear títulos y campos, tanto verticalmente como horizontalmente, y usar el mismo espaciado entre controles
    • Usar siempre el mismo tipo de alineación de etiquetas: si se alinean a la izquierda, o a la derecha más cerca de los datos, hacerlo igual en todos los forms del sistema
    • Todas las acciones que se lancen desde el form deben poder hacerse con un control claramente visible y pensado para tal acción (por ejemplo, un label no está pensado para eso, un botón de comando sí)
    • Evitar el bloqueo de la navegación por la pantalla. Por ejemplo, no deberían mostrarse mensajes modales (tipo messagebox) por el solo hecho de recorrerla. La navegación debería ser libre.
    • Intentar utilizar mecanismos no bloqueantes para mostrar mensajes al usuario. Por ejemplo, la línea de estado inferior, un espacio especial de notificación o una ventana que el usuario pueda invocar con una tecla o botón.
    • Hacer un uso inteligente de los colores y las notificaciones gráficas, como iconos o mensajes. Por ejemplo, ante un error de ingreso por parte del usuario, se puede colorear el fondo de dicho control para que el usuario lo vea y de esta forma no lo bloquea
    • Utilizar el control adecuado para el tipo de ingreso de datos o de selección requerido. Por ejemplo, no usar varios checkbox para simular on optingroup
    • Diseñar dejando el espacio suficiente para los datos. El usuario debería poder ver el dato completo sin necesidad de hacer scroll dentro de un textbox demasiado pequeño, por ejemplo, o debería poder ver una opción seleccionada completa en un combobox sin necesidad de desplegarlo.
    • En los cuandros de diálogo o pantallas de acción que requieran alguna decisión del usuario, poner siempre un botón para Cancelar o Salir, ya que algo como "Aceptar" solamente, no es una opción.
    • Aprovechar la posibilidad de redimensionamiento de los forms y hacer buen uso de la propiedad Anchor, que permite redimensionar los controles
    • Evitar los colores chillones y optar por colores tranquilos (hay guías sobre esto)
    • Dado que hay sistemas en los que el usuario trabaja todo el día, el diseño y la usabilidad deben considerarse como algo muy importante, y evitar elementos que distraigan su atención

    Ejemplos de diseño:


    Ejemplo 1: Mal diseño
    ¿No sería mejor permitir Cancelar?
    Un claro ejemplo de lo que podría ocurrir si no se permite:




    Ejemplo 2: Mal diseño
    Cajas y cajas y cajas dentro de cajas...





    Ejemplo 3: Buen diseño
    Este diseño que permite navegar sin bloquear al usuario con mensajes modales. Rápidamente se pueden ver los campos que tienen algún error (fondo rojo) y un mensaje en una zona especial indica el error ocurrido más inmediato. Además el botón Guardar no se habilita hasta que esté todo correcto, pero siempre se puede Salir y abandonar el proceso. Un botón extra podría permitir ver todos los errores de validación y posibles sugerencias de solución en un form aparte:



    Ejemplo 4: Buen diseño
    Observar a la derecha, un solo cuadro y 3 bloques de información separados por espacio, y un panel inferior separado por la sola presencia de los 4 botones de comando. Todo el conjunto usando tonos pastel entre vívidos y tranquilos, y muy amenos:





    Beneficios:
    • Un recorrido ordenado por la interfaz, permite predictibilidad
    • Una interfaz clara minimiza los errores y aumenta la satisfacción del usuario
    • Un buen diseño permite que el usuario pueda ser más eficiente, pueda hacer las tareas en menor tiempo y pueda distenderse



    13. Usa Control de Versiones

    [Artículo] ¿Para qué sirve? ¿Por qué es necesario?



    Nota final:


    Esto no es todo, hay más, pero esta es una buena base para poder trabajar en un equipo o en solitario, ya que todas estas prácticas tienen como objetivo final el facilitar la comprensión y legibilidad del código, su encapsulación, también minimizar el costo de su mantenimiento y maximizar la satisfacción y eficiencia del usuario.


    Hasta la próxima!