Mostrando entradas con la etiqueta buenas prácticas. Mostrar todas las entradas
Mostrando entradas con la etiqueta buenas prácticas. Mostrar todas las entradas

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!

    viernes, agosto 15, 2014

    Reglas mínimas y buenas prácticas de programación

    Por: Fernando D. Bozzo

    Las reglas que se indican a continuación, son buenas prácticas de programación que sirven, no sólo para trabajar con control de código fuente, sino para toda la programación en general.

    Aunque para algunos sea obvio, para otros no lo es tanto, así que es mejor explicar los motivos de esto.



    ¿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 tienen 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.


    Buenas prácticas de programación


    La siguiente lista de recomendaciones es lo mínimo que conviene tener en cuenta en todos los desarrollos, tanto trabajando en solitario como en equipo.



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

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

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



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

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

    Ejemplos:
    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



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

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

    CLEAR ALL
    CLOSE ALL
    (y ahora se puede hacer el checkin o checkout)



    4. Cambio de changeset (Plastic/VFP)

    Al igual que en el caso anterior, es conveniente hacer el CLEAR ALL / CLOSE ALL, pero además es conveniente revisar y refrescar la vista de Cambios Pendientes, ya que si hubiera archivos modificados, generaría un aviso.



    5. Trabajo en ramas (SCM)

    Regla de oro: Nunca se debe trabajar o modificar en la rama personal de otro usuario, ya que se le puede ocasionar una inconsistencia en el desarrollo. La rama de trabajo personal es como el maletín de cada uno, no se comparte y es actualizada por el propietario exclusivamente.

    Si se quieren compartir ramas, se deberá hacer con ramas que no sean personales (como una rama de una tarea) y solamente se debe actualizar mediante merge o cherry pick, nunca manualmente.



    6. Sesiones de FoxPro

    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), ya que los programas se suelen cachear en la memoria, y al modificar en otra ubicación se podrían provocar problemas de ruta inválida sobre todo en las clases y forms.



    7. Parámetros

    Usar LPARAMETERS en vez de PARAMETERS y si hay que agregar parámetros a un método, agregarlos siempre al final, nunca en medio, que eso rompe la compatibilidad de los programas.

    Ejemplo agregando nuevo parámetro "tnCosto":
    LPARAMETERS tcNombre, tnEdad, tnCosto




    Como todo, esta lista no es definitiva, pero al menos es una buena base para poder trabajar de forma "compatible" con otros desarrolladores.


    Hasta la próxima!


    domingo, enero 26, 2014

    Crear un proyecto FoxPro ¿por dónde comenzar?

    Por: Fernando D. Bozzo

    Aunque esto es algo fácil una vez adquirida cierta experiencia, muchos se pierden y no tiene claro como comenzar u organizar un proyecto.

    ¿Pero crear un proyecto no es simplemente pulsar el botón new / Project y meterle archivos dentro? No, es más que eso, y a continuación vamos a ver una de las formas comunes de hacerlo de acuerdo a las Buenas Prácticas de programación.



    Cosas a tener en cuenta


    Hay dos modalidades de proyectos, los que hacen nuevos desde cero y los que se crean para poner un poco de orden dentro de uno o más programas existentes o que se quieren dejar de ejecutar con DO <programa> y se necesita hacer un EXE o APP o DLL.
    Da igual que modalidad se use, la estructura del proyecto es muy importante, ya que en parte determina la escalabilidad y principalmente la transportabilidad del mismo, ya que un programa o sistema que solo funciona en una ubicación particular y deja de hacerlo si se cambia de directorio o unidad (C,E,G,etc) generará un grave problema de mantenimiento.



    Estructura de directorios para Desarrollar


    Esta es una estructura de directorios típica:

    \raiz           => Dir.principal del PJX y PRG principal (ej: c:\desa\miapp)
         \datos     => Archivos DBF,FPT,CDX,DBC,DCX,DCT,SQL (y texto DB2/DC2)
         \bmps      => Archivos gráficos JPG,PNG,etc
         \forms     => Archivos SCX/SCX (y texto SCA/SC2)
         \clases    => Archivos VCX/VCT (y texto VCA/VC2)
         \include   => Archivos "include" (.H)
         \menus     => Archivos MNX/MNT
         \config    => Aquí puede ir el CONFIG.FPW 
         \prgs      => Archivos PRG
         \reports   => Archivos FRX/FRT/LBX/LBT (y texto FRA/FR2/LBA/LB2)

    La raíz puede estar ubicada en cualquier directorio, y desde la misma se crean una serie de subdirectorios que contendrán los componentes agrupados por tipo. Esto permite ubicarlos más rápidamente y no tenerlos todos juntos de forma desordenada, donde buscar algo se puede complicar bastante. Además esta separación ayuda al mantenimiento separado de los componentes y a evitar ciertos errores de manipulación.

    En la raíz suele haber 2 archivos principales: el proyecto (PJX) y el programa principal de arranque (MAIN.PRG, PRINCIPAL.PRG o como se llame)

    El directorio datos es el directorio de trabajo del usuario, conteniendo todas las tablas, bases de datos, vistas e índices

    Y el resto de directorios indica con su nombre el tipo de componente que contiene



    El programa principal


    En general conviene que el programa principal del proyecto sea un PRG (por ejemplo, MAIN.PRG) y que esté en el directorio raíz junto al proyecto, ya que es donde sea realiza la inicialización del sistema, donde se establece el directorio por defecto y donde se carga el primer form o menú del sistema.

    En el caso de un sistema, este programa debería tener un contenido mínimo y dejar todas las implementaciones y procedimientos a los componentes y librerías, de forma que quede abierta la posibilidad de que, con una buena encapsulación funcional de componentes, se puedan reutilizar objetos en otros sistemas o para ser usados de otra forma, como procesos batch (por lotes).

    El contenido mínimo de MAIN.PRG (o como lo llamen) para un sistema de mantenimiento (altas, bajas, modificaciones) con formularios de usuario podría ser así:

    *-- MAIN.PRG
    *-- Configuración del entorno
    #INCLUDE INCLUDE\MAIN.H  && Define el archivo de inclusión principal de variables nombradas
    PUBLIC glSalir
    glSalir = .F.            && Se debe asignar .T. cuando se quiera salir
    ON SHUTDOWN DO CerrarSistema
    SET TALK OFF             && No mostrar el resultado de los comandos al ejecutarse
    SET SAFETY OFF           && No mostrar avisos de confirmación al sobreescribir datos
    SET DATE DMY
    SET EXCLUSIVE OFF        && No abrir las tablas en Exclusiva, sino SHARED
    SET DELETED ON           && No mostrar registros marcados como eliminados
    SET CENTURY ON
    SET HOURS TO 24
    SET TABLEPROMPT OFF      && No mostrar ventana de selección de tabla si no encuentra el alias
    SET PATH TO "clases;forms;menus;datos;prgs;bmps" && Rutas relativas a la raíz
    _SCREEN.Caption = NOMBRE_SISTEMA
    CD (JUSTPATH(SYS(16)))   && Cambiarse al directorio raíz (donde está MAIN.PRG)
    DO MENUPRINCIPAL.MPR
    DO FORM entrada_datos.scx && Ejecutar el form principal (si hay) o un menú, o ambos
    DO WHILE NOT glSalir
       READ EVENTS           && Poner al sistema en espera de acciones del usuario
    ENDDO

    *-- Cerrar forms abiertos
    FOR I = _SCREEN.FormCount TO 1 STEP -1
        _SCREEN.Forms(I).HIDE()
        _SCREEN.Forms(I).Release()
    ENDFOR


    RELEASE ALL
    CLOSE ALL

    IF _VFP.StartMode <= 1
      RETURN                 && En modo Desarrollo u objeto sale con RETURN
    ENDIF

    QUIT                     && En modo EXE o APP sale con QUIT

    PROCEDURE CerrarSistema
       glSalir = .T.
       ON SHUTDOWN
       CLEAR EVENTS
    ENDPROC
    *-- Fin MAIN.PRG

    Ahora vamos a ver las distintas secciones de este programa



    El archivo de inclusión MAIN.H


    Los archivos de inclusión permiten definir constantes nombradas, esto es, variables de sólo lectura que durante la compilación del programa se reemplazarán en el  sitio donde sean utilizadas.

    Suele tener varios usos, por ejemplo para ejecutar un sistema en modo Depuración o en modo Producción se puede usar una constante DEBUGMODE y luego parametrizar código de programa para que dependiendo de su valor habilite o deshabilite ciertas partes. También se puede usar para la localización de la aplicación, que es poner en constantes todos los textos que muestre la aplicación para poder internacionalizar su interfaz y soportar diferentes idiomas.

    Es conveniente usar un archivo de inclusión único que permita referenciar a otros archivos de inclusión si fuera necesario, cosa que en los forms, clases, programas y demás componentes siempre se referencie al principal y éste se encargue de referenciar a los demás.
    Este sistema de referenciamiento de archivos include dentro del principal da mucha flexibilidad, ya que por un lado evita duplicar las constantes nombradas que ya están definidas en otros archivos, por ejemplo el foxpro.h que viene con FoxPro.

    Este es un ejemplo de archivo de inclusión:

    *-- MAIN.H
    *-- VARIABLES NOMBRADAS Y LINKEO DE OTROS ARCHIVOS INCLUDE

    #INCLUDE \include\foxpro.h
    #DEFINE NOMBRE_SISTEMA   "GESTIÓN"

    #DEFINE DEBUGMODE .T.

    Como se observa, este archivo de inclusión referencia a otro archivo de inclusión (aquí foxpro.h, pero podría referenciar a otros más agregando sentencias #include) y además define una constante para el nombre del sistema que usé para definir el CAPTION de _SCREEN en MAIN.PRG



    Configuración del PATH de la aplicación


    Para que se puedan encontrar los componentes sin tener que referenciarlos con rutas absolutas, es necesario definir el Path, y lo más conveniente es hacerlo con rutas relativas al directorio raíz, tal como puse en MAIN.PRG:

    SET PATH TO "clases;forms;menus;datos;prgs;bmps"

    Esto indica que desde el directorio actual o raíz, busque los componentes en los subdirectorios "clases", "forms", etc.

    La ventaja de definirlo así es que al mover la aplicación a otro directorio, no hará falta hacer ningún cambio en el código, ya que todo permanecerá perfectamente referenciado, sin importar si se mueve incluso a otra unidad de disco o de red, siempre que se mueva la estructura completa de directorios.



    El archivo de configuración CONFIG.FPW


    El CONFIG.FPW es opcional, y se usa para algunos seteos iniciales que se desea ejecutar antes de cualquier instrucción de programa y de que se muestre la pantalla principal de FoxPro. Habitualmente aquí se configuran cosas como indicar que no se desea usar el archivo de recursos (foxuser.dbf), o si se quiere que el sistema no tenga visible la ventana principal de FoxPro y solo muestre los forms del usuario o el principal, o incluso se puede configurar los directorios de temporales y de trabajo o la cantidad máxima de variables de memoria que se usarán.

    Un archivo CONFIG.FPW puede contener esto:

    ALLOWEXTERNAL=OFF
    RESOURCE=OFF
    SCREEN=OFF

    Si se quiere forzar a que todos los temporales vayan a un directorio, solo hay que agregar esto (c:\temp se puede reemplazar por cualquier directorio):

    EDITWORK=C:\TEMP
    PROGWORK=C:\TEMP
    SORTWORK=C:\TEMP
    TMPFILES=C:\TEMP



    Alternativamente también se puede cambiar la ubicación de todos los temporales, pero por usuario, por ejemplo:

    EDITWORK=C:\APLICACION\DATOS\TEMP\USUARIO
    PROGWORK=C:\APLICACION\DATOS\TEMP\USUARIO
    SORTWORK=C:\APLICACION\DATOS\TEMP\USUARIO
    TMPFILES=C:\APLICACION\DATOS\TEMP\USUARIO



    Pero requiere algo más de trabajo, y se puede hacer con 2 archivos config. Para comenzar, se requiere un CONFIG.FPW mínimo dentro del proyecto, por ejemplo:

    ALLOWEXTERNAL=ON
    RESOURCE=OFF
    SCREEN=OFF


    Y el segundo config.fpw se puede hacer usando un programa lanzador (llamémoslo Lanzador.exe) que invoque al EXE principal del sistema (ej: sistema.exe), previa preparación del archivo de configuración.

    Por ejemplo, el lanzador.exe debería hacer lo siguiente:

    1. Crear la carpeta para el usuario, según se obtenga de SYS(0)
    2. Crear un CONFIG.FPW personalizado para ese usuario (como el de arriba con EDITWORK/SORTWORK/etc, pero reemplazando USUARIO por el nombre de usuario obtenido del SYS(0)) y escribirlo en su path particular que se creó en (1)
    3. Finalmente el lanzador invoca al EXE principal con la nueva ruta:
      sistema.exe -cc:\aplicacion\datos\temp\usuario

    De esta forma se pueden separar los temporales de cada usuario, lo que, cuando hay muchos usuarios, previene los errores de temporales con el mismo nombre.


    En la ayuda de FoxPro, buscando por "config.fpw" en el índice, muestra todas las configuraciones.
    Aunque casi todos los seteos se pueden hacer por programa (comandos SET), hay algunos que solamente se pueden hacer aquí.

    Nota: El motivo de que haya puesto el CONFIG.FPW en el directorio CONFIG y no haya incluido este directorio en el Path, es por dos cosas:

    1. Porque cuando ejecuto el programa con DO MAIN.PRG en modo Desarrollo, no quiero que me oculte la pantalla o desactive el archivo de recursos
    2. Porque lo anterior quiero que lo haga solo cuando lo ejecuto como EXE.


    Truco:
    Para esto es fundamental incluir el archivo CONFIG.FPW en el proyecto pero no referenciar su directorio en el Path, de esta forma cuando se ejecuta el programa con DO MAIN.PRG no encontrará el CONFIG.FPW porque su directorio no está en el Path, pero cuando se ejecute como EXE, al estar incluido dentro del proyecto, el EXE encontrará todos los componentes que haya dentro del PJX, aunque no se haya configurado un SET PATH (es como un SET PATH implícito).



    Capturar el cierre desde la cruz (o aspa) de la ventana


    Si se desea capturar este evento para poder cerrar la ventana desde la cruz, como todas las aplicaciones Windows, entonces se puede usar la sentencia ON SHUTDOWN como hice en MAIN.PRG, que normalmente llama a un programa de cierre que configura una variable global para salir y hace un CLEAR EVENTS.

    Es importante tener en cuenta que para que esto funcione no deben haber formularios modales abiertos. Solo puede haber abiertos forms normales (no modales).



    Configuración del directorio principal o raíz


    En el ejemplo está en MAIN.PRG y se encarga de hacer que el directorio por defecto de la aplicación sea el mismo donde está MAIN.PRG, por eso la importancia de que este programa principal esté en el directorio raíz y no dentro de PRGS con el resto de programas.

    CD (JUSTPATH(SYS(16)))

    La función SYS(16) devuelve el nombre del programa en ejecución con su ruta absoluta. Por ejemplo, si el programa de nuestro ejemplo está en el directorio c:\desa, SYS(16) devolverá C:\DESA\MAIN.PRG, y si fuera un ejecutable y el proyecto se llamara SISTEMA.EXE devolvería C:\DESA\SISTEMA.EXE
    Con la función JUSTPATH() obtenemos la ruta del programa y finalmente con CD (Change Directory) nos cambiamos al directorio obtenido.



    Ejecución de un form o menú (o ambos)


    Antes de entrar al bucle READ EVENTS, se ejecuta el form o menú que se quiera mostrar al usuario. ¿Y por qué un bucle y no solo Read Events? Bueno, esto es opcional y lo ví así en algunos ejemplos de FoxPro. El posible motivo que encuentro para esto es que si se quiere controlar la salida exclusivamente con la variable pública glSalir y se tiene la costumbre de ejecutar CLEAR EVENTS en el cierre de todos los formularios, esta es un buena forma de controlar de que se vuelva a ejecutar el READ EVENTS y no se salga del sistema si no se elije expresamente salir, lo que pondrá glSalir = .T. y hará el CLEAR EVENTS definitivo.
    Por qué se pueda querer hacer un CLEAR EVENTS en cada form no lo sé, pero podría haber algún caso especial que lo requiera y este método lo controla.

    DO WHILE NOT glSalir     && No salir con Clear Events hasta que glSalir sea .T.
       READ EVENTS           && Poner al sistema en espera de acciones del usuario
    ENDDO






    Cerrar los forms abiertos


    Esto es útil cuando se tiene un sistema MDI (Múltiple Document Interface) con varias ventanas que pueden estar abiertas a la vez y se quiere poder salir y cerrarlas todas sin tener que estar cerrando una a una.

    *-- Cerrar forms abiertos
    FOR I = _SCREEN.FormCount TO 1 STEP -1
        _SCREEN.Forms(I).HIDE()
        _SCREEN.Forms(I).Release()
    ENDFOR


    El motivo de que el conteo sea en orden inverso es el mismo motivo de que la eliminación de elementos de un array debe hacerse de la misma forma: Si hay 5 elementos en el array (o 5 forms en el array de forms) y se quisieran borrar en orden normal, al llegar al  4 ya no existe, porque se borraron 3, y 5 - 3 = 2, con lo cuál daría un error, y para evitar esto se hace en orden inverso.



    Retorno final o cierre de sesión


    Finalmente se debe devolver el control al sistema... o a la ventana de comandos. Veamos: Si estamos depurando y ejecutando con DO MAIN.PRG, no queremos que se nos cierre la sesión de FoxPro, sino que queremos permanecer en la misma para seguir trabajando, hacer pruebas en la ventana de comandos, etc., pero si estamos en el EXE y este se ejecutó con doble click en el explorador de archivos o se lanzó desde otro programa, no hay ventana de comandos a la que volver, y se debe cerrar la sesión para que no quede una "sesión colgada" donde la ventana de FoxPro no se vé pero se sigue ejecutando (se puede ver con el Administrador de Tareas)

    ¿Cómo distinguir si estamos ejecutando con DO MAIN.PRG o con el ejecutable? esto lo hacemos consultando la propiedad StartMode, así:

    _VFP.StartMode

    En la ayuda figuran todos sus valores, pero nos sirve saber que 0=Ejecución con DO, 1=Ejecución con CREATEOBJECT y el resto de valores son ejecución como EXE, DLL, etc.




    Distribución de ejecutables



    En el caso de que se quiera hacer un ejecutable (EXE) o APP para distribuir, y se use algún instalador, típicamente el directorio de instalación del programa será "Program Files" o "Archivos de programa", pero los datos nunca deberán estar allí, ya que desde Windows 7 ese es un directorio especialmente protegido contra escritura.

    Los datos deberán ubicarse en otro directorio que no esté dentro de "Program Files" ni otro directorio del Sistema, como Windows o System32. Cualquier directorio es válido. En este caso, puede que se complique un poco indicar que los programas van en un directorio y los datos en otro que no está bajo el, y además se deberá tener en cuenta para la definición de los PATH de datos, que deberán parametrizarse.

    Otra opción, que para mí es mejor, es instalar directamente el programa en otro directorio, por ejemplo, c:\app\mi_sistema, para que pueda mantener la misma estructura de directorios que en desarrollo, y así evitar complicaciones.



    Finalizando


    Hemos visto una de las formas (hay otras) de crear un proyecto, de definir un programa principal, archivos de inclusión y una estructura transportable usando rutas relativas. Lo único que queda es crear el resto de los componentes (forms, clases, reportes, etc) desde la ventana del proyecto, pero eso ya lo dejo a vuestro criterio.


    Hasta la próxima!



    martes, enero 14, 2014

    Desmitificando el control de errores con Try/Catch

    Por: Fernando D. Bozzo

    He visto en muchas ocasiones que hay como cierto miedo a tratar los errores, como si fuera algo complejo, difícil de controlar o directamente como si los errores "no fueran a ocurrir", que es peor, porque no se controlan y se confía ciegamente en la suerte.

    En esta entrada voy a presentar una forma de trabajar con el Try/Catch que cumple dos objetivos distintos con una misma rutina:
    1. Poder relanzar el error para que lo trate un nivel superior
    2. Poder tratar el error localmente sin relanzarlo
    Y lo mejor de todo es que este comportamiento se puede elegir "desde fuera" del procedimiento, antes de llamarlo, de modo que un mismo procedimiento se puede usar de las dos formas, dependiendo de cómo se utilice.

    ¿Pero qué sentido tiene poder hacer esto? ¿Para qué puede servir llamarlo de una forma u otra?


    Imaginemos dos situaciones distintas:

    Situación 1: Tenemos una rutina en la que si ocurre un error, queremos mostrarlo para que el usuario esté notificado y nada más

    Situación 2: Tenemos un método de negocio (procedimiento sin interfaz ni mensajes que se muestren al usuario que realiza alguna operación), donde de ocurrir un error, y al no poder mostrarlo, la única salida es reenviarlo hacia el nivel anterior para que sea tratado en el origen, por ejemplo, el botón de comando que originó todo, o un programa externo.

    Normalmente esto requiere dos tratamientos distintos, pero hay una solución muy simple que permite utilizar una u otra de acuerdo a lo que nos convenga, que es la siguiente:

    PROCEDURE Proc_X( toEx as Exception, tlRelanzarError, otros_params )
       TRY
          toEx = NULL
          * Aquí va el proceso
     

       CATCH TO toEx
          IF tlRelanzarError
             THROW
          ENDIF

       FINALLY
          * Este IF/MESSAGE/ENDIF sólo debe ir si este es un método de Interfaz!

          IF NOT ISNULL(toEx) AND NOT tlRelanzarError
             MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
          ENDIF

          * Recolección de basura
       ENDTRY
    ENDPROC




    En azul y amarillo están resaltados los dos parámetros que permiten esto. Por un lado, toEx guarda la información del error y por el otro tlRelanzarError indica si se desea, o no, relanzar el error al nivel superior. No hay más! Y así de simple como se ve, es sumamente versátil.

    Veamos algunos ejemplos en la práctica.


    Ejemplo 1: Tratamiento de errores local

    En este caso se quiere controlar el error y mostrar un mensaje al usuario. Los métodos de este tipo suelen ser de la interfaz visual, y como se ve, es parecido al ejemplo original, pero más recortado y sin la parte de relanzar:


    PROCEDURE Form1.cmdEjecutar.CLICK
       TRY
          LOCAL loEx as Esception
          loEx = NULL
          * Aquí va el proceso
       CATCH TO loEx

       FINALLY
          IF NOT ISNULL(loEx)
             MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
          ENDIF

          * Recolección de basura
       ENDTRY
    ENDPROC




    Dentro de este mismo tipo de tratamiento tenemos otro caso, una actualización que solo actualiza un campo TIMESTAMP que se llama con un ON KEY LABEL F2 y que, por llamado con una tecla ON KEY, no permite otro tratamiento de errores que no sea local:

    ON KEY LABEL F2 DO Actualiza WITH "", .F., 0

    PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
       TRY
          LOCAL lnCodError
          lnCodError = 0 
          toEx = NULL 
          UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
          tnCount = _TALLY

       CATCH TO toEx
          IF tlRelanzarError
             THROW
          ENDIF
          lnCodError = toEx.ErrorNo

       FINALLY
          IF NOT ISNULL(toEx) AND NOT tlRelanzarError
             MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
          ENDIF

          USE IN (SELECT("USUARIO"))
       ENDTRY
       RETURN lnCodError
    ENDPROC



    Como se ve, desde el DO Actualiza se está indicando .F. para tlRelanzarError, por lo que en el FINALLY, si ocurrió un error, se mostrará el mensaje con sus datos. Además en el tercer parámetro se pasa 0 porque no se podrá leer su valor.




    Ejemplo 2: Tratamiento de errores en nivel superior

    En este caso vamos a usar los ejemplos anteriores, pero encadenados de tal forma que el segundo (la actualización) cambia su forma de trabajar para devolver el error y dejar que que sea el nivel superior quien muestre los datos del mismo:


    PROCEDURE Form1.cmdEjecutar.CLICK
       TRY
          LOCAL loEx as Esception, lnCount
          loEx = NULL
          DO Actualiza WITH "", .T., lnCount
          IF lnCount = 0
             MESSAGEBOX( "No se actualizaron registros!" )
          ENDIF

       CATCH TO loEx

       FINALLY
          IF NOT ISNULL(loEx)
             MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
          ENDIF

          * Recolección de basura
       ENDTRY
    ENDPROC


    PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
       TRY
          LOCAL lnCodError
          lnCodError = 0 
          toEx = NULL 
          UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
          tnCount = _TALLY

       CATCH TO toEx
          IF tlRelanzarError
             THROW
          ENDIF
          lnCodError = toEx.ErrorNo

       FINALLY
          IF NOT ISNULL(toEx) AND NOT tlRelanzarError
             MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
          ENDIF

          USE IN (SELECT("USUARIO"))
       ENDTRY
       RETURN lnCodError
    ENDPROC



    En este caso, si ocurriera un error en la rutina "Actualiza" se relanza el error (THROW) y se haría la recolección de basura (cerrar la tabla, limpiar vars, etc) tanto haya error o no.
    El Throw salta hasta el CATCH del nivel anterior, que es el cmdEjecutar.Click() y recién ahí es donde se mostraría.

    Así es como la misma rutina "Actualiza" se puede reutilizar controlando el error de dos formas distintas.


    Control de errores compacto


    Habiendo visto los casos de uso anteriores surgen varias ideas y optimizaciones para casos más concretos, por ejemplo, si sabemos de antemano que un método jamás mostrará un error porque es de negocio o para un proceso batch, entonces la estructura podría cambiar a esto:

    PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
       TRY
          LOCAL lnCodError
          lnCodError = 0 
          toEx = NULL 
          UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
          tnCount = _TALLY

       CATCH TO toEx
          IF tlRelanzarError
             THROW
          ENDIF
          lnCodError = toEx.ErrorNo

       FINALLY
          USE IN (SELECT("USUARIO")) 

       ENDTRY

       RETURN lnCodError
    ENDPROC




    Lo que sigue dejando 2 opciones:
    1. Llamar al método Actualiza relanzando el error, que lo redirigirá al Catch del nivel superior
    2. Llamarlo sin relanzar el error para evaluarlo localmente, como el siguiente caso

    DO Actualiza WITH loEx, .F., lnCount
    IF NOT ISNULL(loEx)
       *-- Ocurrió un error, hacer algo
    ELSE
       *-- Ejecución OK
    ENDIF




    Achicando aún más


    Finalmente hay una forma aún más compacta, asumiendo que solo se quiere relanzar el error y nunca se tratará como el caso anterior, sino directamente en el Catch del nivel superior que corresponda:

    PROCEDURE Actualiza( tnCount )
       TRY
          LOCAL loEx as Exception
          lnCodError = 0 
          loEx = NULL 
          UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
          tnCount = _TALLY

       CATCH TO loEx
          THROW
       FINALLY
          USE IN (SELECT("USUARIO"))   ENDTRY
       RETURN
    ENDPROC







    Ayuda para Depurar en el control de errores


    Esto es algo que no suele tenerse en cuenta, y que debería, ya que permite encontrar los errores mucho antes.
    Cuando ocurre un error y se relanza la excepción hacia los niveles superiores, se pierde la información del entorno donde ocurrió (tablas abiertas, variables creadas, etc), ya que cada nivel fue cerrando y limpiando sus cosas, entonces se hace necesario una forma rápida que permita poder depurar "in-situ" cuando se está ejecutando en modo Desarrollo. Esta es una forma de hacerlo:

    PROCEDURE Actualiza( tnCount )
       TRY
          LOCAL loEx as Exception
          lnCodError = 0 
          loEx = NULL 
          UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
          tnCount = _TALLY

       CATCH TO loEx
          IF _VFP.StartMode = 0 THEN
             SET STEP ON
          ENDIF
          THROW
     

       FINALLY
          USE IN (SELECT("USUARIO")) 

       ENDTRY

       RETURN
    ENDPROC





    Es así de simple y muy efectivo. Incluso se podría encapsular ese bloque en una función externa reutilizable, que también se encargue de generar un Log.


    Una cosa importante sobre THROW:

    No es lo mismo esto:

    THROW


    que esto:

    THROW loEx



    El primero relanzará el error tal cual, y se podrán seguir leyendo sus datos en loEx en cada Catch por el que pase, mientras que el segundo está relanzando específicamente un objeto Exception que al Catch de nivel superior le llegará en la propiedad UserValue, lo que lo hace más difícil de tratar si no se sabe esto previamente.


    Información de error complementaria muy útil

    Hay una propiedad del objeto Exception que suele pasarse por alto y que resulta ser muy útil para agregar información específica, que es UserValue. Obviamente que para aprovecharla se deben relanzar los errores con Throw solamente, o de otro modo se pisará su valor con una Excepción.

    Lo que le da un valor especial es que al ser una propiedad "para el usuario" podremos poner allí lo que querramos, tanto un valor como un objeto como un XML, lo que sea, y esto permite agregar más información específica del sitio donde ocurrió el error.

    Por ejemplo, en el procedimiento AnalizarBloque_CDATA_inline() de FoxBin2Prg encontramos esto:

    CATCH TO loEx
        IF loEx.ErrorNo = 1470    && Incorrect property name.
            loEx.UserValue    = 'PropName=[' + TRANSFORM(tcPropName) + '], Value=[' ;

               + TRANSFORM(lcValue) + ']'
        ENDIF



    En este caso concreto me resulta muy importante adjuntar al objeto del error el nombre y el valor de la propiedad que puede causarlo, ya que si no, no saldría reflejado en ningún otro sitio o a lo sumo saldría solo el nombre de la propiedad.

    Luego, en el nivel superior, junto la información de esta forma para mostrarla o loguearla a un archivo:

    lcError = 'Error ' + TRANSFORM(toEx.ERRORNO) + ', ' + toEx.MESSAGE + CR_LF ;
        + toEx.Procedure + ', ' + TRANSFORM(toEx.LineNo) + CR_LF ;

        + toEx.LineContents + CR_LF + CR_LF ; 
        + toEx.StackLevel + CR_LF + CR_LF ; 
        + toEx.Detailt + CR_LF + CR_LF ;
        + EVL(toEx.UserValue,'')



    En el método escribirArchivoBin() de la clase c_conversor_prg_a_dbf, guardo datos sobre el índice, el campo y la sentencia de creación de la tabla, ya que se intentará crear en ese método, pero si falla necesito saber qué ocurrió:

    loEx.UserValue = 'lcIndex="' + TRANSFORM(lcIndex) + '"' + CR_LF ;
        + 'lcFieldDef="' + TRANSFORM(lcFieldDef) + '"' + CR_LF ;
        + 'lcCreateTable="' + TRANSFORM(lcCreateTable) + '"'




    Para cada caso se debe contemplar adjuntar información específica para que ubicar el error, en caso de ocurrir, se haga lo más rápido posible.


    Conclusión

    Vimos algunas técnicas para controlar los errores, que es una de las cosas más importantes en cualquier aplicación, ya que de ello depende que aparezca un error descontrolado y que el usuario decida si "Aceptar, Cancelar o Ignorar" (lo peor que puede pasar, sobre todo si "Ignora"), o que el sistema tenga el flujo de errores definido desde el método más profundo para reenviarlo hasta llegar la interfaz o al programa externo.

    FoxBin2Prg, por ejemplo, usa el control compacto de errores, ya que no me interesa mostrar mensajes en cualquier momento, e incluso se puede configurar para no mostrar ningún mensaje y opcionalmente generar un log.

    La recolección de basura también es muy importante, ya que no hacerlo de forma correcta puede llevar a un sistema inestable o que a la larga genere el temido error C0000005

    Hasta la próxima!


    Relacionados:
    Técnicas de programación en VFP: Procesos desatendidos y control de progreso visual