domingo, diciembre 14, 2014

Whitepaper: Cacheo de lectura y bloqueo oportunista en redes Microsoft Windows

Traducción del artículo "Opportunistic Locking and Read Caching on Microsoft Windows Networks", de Dennis Piccioni. Última edición: septiembre 6, 2013


N.del T.: Aunque no se menciona a Visual FoxPro, se menciona un sistema de base de datos similar (Visual DataFlex), por lo que aplican cada uno de los problemas y soluciones.


Nota 2: El cacheo es un almacenamiento temporal en memoria caché (RAM).



Resumen


Las redes Windows configuradas inapropiadamente puede conducir a la corrupción en los datos en cualquier sistema de archivos de base de datos, incluyendo la base de datos (DataFlex) embebida. Dos comportamientos de red de Windows, el bloqueo oportunista (en servidores Windows) y el cacheo de lectura (en clientes Windows) son fuentes de posible corrupción.

Se proporciona este documento para los clientes de Data Access Worldwide (DAW) para discutir estos comportamientos, sus efectos y qué se puede hacer para reducir al mínimo las posibilidades de corrupción de datos en las redes de Windows cuando se ejecuta Visual DataFlex (VDF) y/o aplicaciones DataFlex, y centralizar la información en un solo lugar.
 
La información contenida en este documento es una compilación de la última información disponible sobre estos temas desde Microsoft, nuestros propios informes de pruebas en la empresa y los clientes. Estamos tratando de combinar la escasa información proporcionada mi Microsoft sobre estos temas en un solo lugar. Por favor revisar este documento de vez en cuando para comprobar la información actualizada. La fecha corregida por último en la parte superior del papel reflejará cuando se hicieron las últimas ediciones.
 
La información contenida en este documento sólo se ocupa de los sistemas operativos que actualmente apoyamos. Usted puede ver información acerca de productos y entornos admitidos en la lista de productos soportados por Data Access Worldwide .

 

Contenido

  • ¿Qué es el bloqueo oportunista?
  • ¿Qué es el Cacheo de Lectura?
  • ¿Qué son SMB2 y SMB3?
  • Recomendaciones de Data Access Worldwide
  • ¿Qué sistemas operativos están afectados?
  • Qué Entornos no están afectados?
  • Haciendo Cambios en el Registro de Windows
  • Desactivando Oplocks en PC Clientes Windows
  • Desactivando Oplocks en Servidores Windows
  • Desactivando Oplocks en SMB2 y SMB3
  • ¿Las prácticas de codificación afectan a estas cuestiones?
  • Corrupción de datos Persistente
  • Términos
  • Recursos
 

¿Qué es el bloqueo oportunista?


El bloqueo oportunista (oplocks) es un mecanismo específico para Windows para bases de datos cliente/servidor para permitir que varios procesos bloqueen el mismo archivo mientras permite el almacenamiento de datos local (cliente) en caché para mejorar el rendimiento sobre redes Windows.  

Desafortunadamente, la configuración predeterminada del mecanismo oplocks que mejora el rendimiento de un tipo de base de datos (cliente/servidor) también presenta problemas de integridad de datos para otros tipos de bases de datos (sistema de archivos/ISAM ).

La documentación de Microsoft indica que "un bloqueo oportunista (también llamado oplock) es un bloqueo puesto por un cliente en un archivo que reside en un servidor. En la mayoría de los casos, un cliente solicita un oplock así puede almacenar en caché los datos localmente, lo que reduce el tráfico de red y la mejora de tiempo de respuesta aparente. Los Oplocks son utilizados por los redirectores de red en los clientes con servidores remotos, así como por las aplicaciones de cliente en servidores locales" y "los Oplocks son solicitudes del cliente al servidor. Desde el punto de vista del cliente, son oportunistas. En otras palabras, el servidor garantiza dichos bloqueos cuando otros factores hacen esos bloqueos posibles".

Puedes leer más sobre oplocks en la documentación de Microsoft. Por favor vea la Recursos sección para obtener más información.


 

¿Qué es el Cacheo de Lectura?


El cacheo de lectura, a veces conocido como lectura anticipada en caché, es una característica de bloqueos oportunistas. Es una técnica utilizada para acelerar el acceso de red a los archivos de datos. Se trata de cachear datos en los clientes en lugar de en los servidores cuando sea posible.

El efecto del cacheo local es que permite que múltiples operaciones de escritura en la misma región de un archivo que se combinan en una sola operación de escritura a través de la red. El cacheo local reduce el tráfico de red, ya que los datos se escriben una vez. Dicho cacheo mejora el tiempo de respuesta aparente de aplicaciones porque las aplicaciones no esperan a que los datos se envíen a través de la red al servidor.
 
Los problemas con el cacheo de lectura por lo general se producen si algo inesperado sucede, como un cuelgue de la estación de trabajo, donde los datos no se flushean debidamente desde la estación de trabajo, lo que puede llevar a corrupción en los datos.

La documentación de Microsoft señala que «En condiciones extremas, algunas aplicaciones de bases de datos multiusuario que utilizan un almacén de datos común a través de una conexión de red en un servidor de archivos, pueden experimentar problemas de integridad transaccional o la corrupción de los archivos y/o índices de base de datos almacenados en el servidor. Normalmente, esto se aplica a las aplicaciones de bases de datos multiusuario "estilo ISAM" u "orientadas a registros", y no a sistemas relacionales cliente/servidor como SQL Server» y que «un peligro del cacheo local es que los datos escritos sólo tienen tanta integridad como el propio cliente, en tanto que los datos se cachean en el cliente. En general, los datos cacheados localmente se deben flushear al servidor lo antes posible.»
 
Puedes leer más sobre el almacenamiento en caché leer en la documentación de Microsoft.


 

¿Qué son SMB2 y SMB3?


SMB2 y SMB3 son la segunda y tercera generación, respectivamente, de las comunicaciones de bloque de mensajes de servidor (SMB) en redes Windows.

SMB2 se introdujo en Windows Vista, 7 y Windows Server 2008 para permitir una comunicación más rápida entre los equipos que ejecutan Windows Vista, 7 y Windows Server 2008. SMB3 se introdujo en Windows 8 y Server 2012.

Las versiones anteriores de Windows (Windows XP, Server 2003 y anteriores) utilizaban SMB1, también llamado SMB "tradicional". SMB1 aún se soporta en las versiones actuales de Windows (Vista, 7, 8, Server 2008, Server 2012) para la compatibilidad con versiones anteriores.


 

Recomendaciones de Data Access Worldwide


La base de datos embebida (DataFlex) es una base de datos ISAM y por lo tanto susceptibles a los efectos de la configuración predeterminada de Windows oplocks. Usar la base de datos embebida en las redes de Windows sin desactivar oplocks no se recomienda ni se soporta y tiene una alta probabilidad de corrupción de datos.
 
La mejor integridad de los datos, seguridad y rendimiento está disponible mediante el uso de una base de datos cliente/servidor, como IBM DB2, Microsoft SQL Server o Pervasive.SQL con sus aplicaciones de Visual DataFlex y DataFlex. Data Access Worldwide tiene drivers directos (Kits de Conectividad) disponibles para IBM DB2 , Microsoft SQL Server y Pervasive.SQL , así como un kit de conectividad ODBC para el acceso a las bases de datos compatibles con ODBC. Todos estos controladores se cargan en tiempo de ejecución y no requieren cambios de codificación para utilizar con las aplicaciones existentes VDF, DataFlex o aplicaciones web del servidor.
 
Se puede lograr una operación confiable de base de datos en redes de Windows utilizando la base de datos embebida, siempre que la red está configurada correctamente. Puede utilizar la información en este documento para configurar los parámetros de oplocks de su red de Windows. Una desventaja de utilizar este método son los problemas de mantenimiento: se debe asegurar continuamente de que todos y cada uno de los servidores y clientes que utilicen una aplicación que acceda a la base de datos embebida, tengan desactivado oplocks y que siempre se mantengan en ese estado.
 
Un método para asegurar que oplocks está desactivado en los PC cliente es agregar código para aplicaciones que compruebe esos ajustes al inicio de la aplicación. La gente de VDF-Guidance.com ha creado un proyecto de código abierto llamado regcheck para este propósito.
 
Desactivar Oplocks puede tener un impacto en el rendimiento de las redes de Windows.
 
Los Oplocks no se aplican a las bases de datos cliente-servidor. DAW no hace ninguna recomendación específica sobre oplocks si utiliza una base de datos cliente-servidor y no hay tablas de base embebidos.
 
Este artículo le dirá cómo deshabilitar los bloqueos oportunistas, pero debido a las razones expuestas anteriormente Data Access Worldwide recomienda el uso de una base de datos cliente-servidor para las aplicaciones multi-usuario DataFlex en redes Windows.


 

¿Qué sistemas operativos están afectados?


Todos los equipos que ejecutan sistemas operativos Windows que hospeden o accesen tablas de bases de datos embebidas accedidas por otras PCs con Windows necesita tener oplocks dasactivado con el fin de minimizar las posibilidades de corrupción de base de datos.
 
Los Oplocks se pueden desactivar en uno (o ambos) de los siguientes:
  • el lado del cliente (un PC con Windows que tiene acceso a una tabla de base de datos embebida alojado en otro PC)
  • el lado del servidor (un PC con Windows que aloja una mesa de base de datos embebida que se accede desde otro PC)

La lista del sistema operativo Windows que actualmente soportamos para nuestros productos incluye Windows 7, Windows 8, Windows 10, Windows Server 2008 y Windows Server 2012.


 

¿Qué Entornos no están afectados?


Hay algunos entornos y escenarios que soportamos que no pueden verse afectados por bloqueos oportunistas, incluso si se utiliza la base de datos embebida:
 
  • Acceso de base de datos local
En general, cada vez que se accede a una tabla de base de datos embebida en el mismo PC donde se encuentra ubicada, los oplocks no se aplican.
  • Windows Terminal Services y Citrix
En condiciones de uso normal para estos entornos, los usuarios inician sesión en un servidor de Windows y ejecutan aplicaciones de forma local en ese servidor. Si, sin embargo, una base de datos embebida se encuentra en otro servidor del que ejecuta WTS/Citrix, los oplocks entre el servidor WTS/Citrix y el servidor de base de datos deben ser desactivados.
  • Aplicaciones de Servidor Web / Servicios Web
En las aplicaciones web los usuarios acceden a un navegador web que solicita datos desde una aplicación Web y/o los datos se solicitan a través de un servicio web. En ambos casos, la aplicación web en un servidor web tiene acceso a la base de datos, el cliente no lo tiene. Si los datos se encuentran en el mismo servidor, oplocks no entran en juego. Si, sin embargo, una base de datos embebida se encuentra en otro servidor del que ejecuta la aplicación web, los oplocks entre el servidor web y el servidor de base de datos deben ser desactivados.



Haciendo Cambios en el Registro de Windows


Los temas siguientes abordan la edición de cambios en el Registro de Windows.
 
Precaución: La siguiente advertencia aparece en cada artículo de Microsoft que discute la edición del Registro de Windows:
 
ADVERTENCIA: Puede editar el registro mediante el Editor del Registro (Regedit.exe o Regedt32.exe). Si utiliza el Editor del Registro incorrectamente, puede provocar problemas graves que conlleven la reinstalación del sistema operativo. Microsoft no garantiza que los problemas que le causan mediante el Editor del Registro de forma incorrecta se pueden resolver. Utilice el Editor del Registro bajo su propio riesgo.

Si cambia alguno de los valores del registro discutidos a continuación, tendrá que reiniciar el PC en el que se ha cambiado el valor para asegurar que la nueva configuración entre en efecto.
 
Los cambios en el registro se listan en el formato MainRegistryKey \ subclave \ subclave RegistryValue = RequiredValue
 
donde:
  • MainRegistryKey es una de las principales claves del Registro de Windows (por ejemplo HKey_Local_Machine)
  • Subclave es cualquier subclave de una clave principal Registro
  • RegistryValue es un valor del Registro para cambiar o añadir en la clave de registro especificada
  • RequiredValue es la RegistryValue valor debe ajustarse para causar el efecto descrito

Si no existen las subclaves o valores que se describen en el Registro, que tendrá que agregarlos. Por favor, revise cuidadosamente antes de hacerlo.


 

Desactivando Oplocks en PC Clientes Windows


Para deshabilitar los bloqueos oportunistas en un PC cliente de Windows (un PC con Windows que tiene acceso a una tabla de base de datos embebida alojada en otro PC), cambiar o añadir los siguientes valores del registro:
 
  • HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MRxSmb\Parameters OplocksDisabled = 1



Desactivando Oplocks en Servidores Windows


Para deshabilitar los bloqueos oportunistas en un servidor de Windows (un PC con Windows que hospeda una tabla base de datos embebida que se accede desde otro PC), cambiar o añadir los siguientes valores del registro:
 
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters EnableOplocks = 0



Desactivando Oplocks en SMB2 y SMB3


Oplocks no se puede apagar para SMB2 y SMB3. Puede deshabilitar SMB2 y SMB3 en sí mismos, cómo hacerlo está documentado por Microsoft en el artículo 2696547 de Knowledge Base .
 
De acuerdo con ese artículo, SMB2 y SMB3 se pueden desactivar en los sistemas operativos Windows compatibles con estos.
 
Para deshabilitar SMB2 y SMB3 en un Windows Vista, 7, 8, Server 2008 o Server 2012 PC que alberga tablas de bases de datos embebidas, cambie o agregue el siguiente valor del Registro:
 
  • HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Services \ LanmanServer \ Parameters SMB2 = 0

Una vez SMB2 y SMB3 están desactivados, SMB1 debe volver a habilitarse para ser utilizado de nuevo y los métodos descritos anteriormente se aplican para desactivar oplocks para SMB1.
 
Para volver a habilitar SMB1 en un Windows Vista, 7, 8, Server 2008 o Server 2012 PC que alberga tablas de bases de datos embebidas, cambio o agregue el siguiente valor del Registro:
 
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters SMB1 = 1



¿Las prácticas de codificación afectan a estas cuestiones?

 
  • Si su código de aplicación utiliza DataDictionaries y/o Data_Sets, no debe haber problemas de integridad de datos después de que los bloqueos oportunistas se han desactivado.
  • Los clientes han informado de que con el código de aplicación que no utilice diccionarios de datos y/o conjuntos de datos (por ejemplo, en un bucle de Búsqueda que utiliza la memoria intermedia para encontrar registros), los datos en los registros nuevos o editados desde que los datos fueron accedidos por primera vez, seguirán sin recuperarse correctamente, incluso con oplocks deshabilitadas. La solución alternativa para esta condición es hacer la búsqueda en un estado de bloqueo o la emisión de un comando que vuelva a leer después de que se encontró cada registro (recuerde que debe emitir un comando de desbloqueo después de una relectura, ya que la relectura realiza un bloqueo como parte de su funcionalidad). Vamos a publicar cualquier información adicional que obtengaos acerca de cómo solucionar este problema en el sistema operativo de Microsoft en cuanto esté disponible.
  • Hemos tratado de utilizar la función de API Win32 FlushFileBuffers de Windows que Microsoft recomienda en su documentación del runtime de DataFlex/ Visual DataFlex cuando el atributo DF_HIGH_DATA_INTEGRITY estaba activado. Sin embargo, el rendimiento de aplicaciones se ha degradado hasta el punto de que era prácticamente inutilizable al hacerlo, porque esta función API de Windows es una llamada muy genérica que flushea todos los buffers en un PC cliente en lugar de sólo los utilizados por una aplicación.


 

Corrupción de datos Persistente


Si ha aplicado a todos los valores que se tratan en este trabajo, pero los problemas de corrupción y otros síntomas persisten, he aquí algo de información adicional:
 
  • Tenemos informes creíbles de los desarrolladores de que hardware de red defectuoso, como una sola tarjeta de red defectuosa, puede causar síntomas similares a la corrupción de datos.
  • Si usted ve la corrupción de datos persistentes incluso después de repetidas reindexaciones, es posible que tenga que reconstruir las tablas en cuestión. Esto implica la creación de una nueva tabla con la misma definición que latabla a ser reconstruida y transferir los datos de la tabla antigua a la nueva. Hay varios métodos conocidos para hacer esto que se pueden encontrar en nuestra base de conocimientos .

 

Términos

 
  • ISAM El Método de Indexado de Acceso Secuencial es un sistema de gestión de archivos desarrollado en IBM que permite acceder a los registros de forma secuencial (en el orden en que se ingresaron) o aleatoria (con un índice).
  • SMB El Protocolo de Bloque de Mensajes de Servidor (SMB) es un protocolo de uso compartido de archivos de red, y como se aplica en Microsoft Windows se conoce como protocolo Microsoft SMB. Si desea conocer más acerca de SMB, consulte la documentación de Microsoft.
 
Es posible que desee comprobar si hay una versión actualizada de este documento de vez en cuando. Muchos de nuestros whitepapers se actualizan cuando cambia la información. Para estos documentos, la fecha de Última edición está siempre en la parte superior del documento.


 

Recursos

 
  • Regcheck Una de las mejores maneras de asegurar que los oplocks están desactivadas en los PC cliente es añadir código para aplicaciones que comprueba los ajustes en el arranque. La gente de VDF-Guidance.com han creado un proyecto de código abierto llamado regcheck para este propósito. Tenga en cuenta que el código de este proyecto no se verifica o mantenida por DAW.
  • Soporte de Data Access Worldwide
    Visita la página de inicio de soporte DAW para obtener información sobre todas nuestras ofertas de soporte, incluyendo la lista de productos admitidos, formularios de informe de errores y ofertas de soporte libres, como la base de conocimientos,Whitepapers y foros de apoyo entre compañeros.







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


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.4 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.38 de FoxBin2Prg (novedades y mejoras)

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.38 de FoxBin2Prg con los siguientes cambios:

  • Nuevo: Agregada posibilidad de usar directamente FoxBin2Prg para las conversiones "Bin2Prg" y "Prg2Bin" desde el explorador de archivos, y evitar el uso de scripts vbs, que en ciertas circunstancias pueden dar problemas o no estar permitidos por el departamento de IT. (Francisco Prieto).
    • La ejecución de vbs (Visual Basic Script) a veces puede dar algunos problemas, como por ejemplo, requerir una confirmación extra del sistema de seguridad de Windows (UAC) o incluso puede que esté inhabilitada su ejecución por el Departamento de IT de la Emprasa. Por este motivo es que en esta versión se ha agregado una parametrización especial para poner en el propio shortcut (atajo o acceso directo) para poder invocar directamente a foxbin2prg.exe. Estos 3 accesos directos a foxbin2prg.exe pueden sustituir a los que se usaban para los scripts vbs, según se explica en el README.txt, y además tiene la ventaja de que el proceso de archivos es más homogéneo y simplificado, al realizarse todo en el mismo paso.
  • Mejora: Mejorado el soporte multilingüe mediante 3 sistemas: 1=Automático según VERSION(3); 2=Por parámetro, usando el nuevo método ChangeLanguage(); 3=Mediante la lectura del archivo foxbin2prg.h
    • Desde ahora la selección de idioma de los mensajes y Logs es automática, mediante la consulta de la función Version(3) de VFP. Esto funcionará bien con el EXE, pero probablemente no lo haga con la versión PRG, ya que esta usa el IDE que está en Inglés, así que la mayoría no tendrá de qué preocuparse y ya no hará falta recompilar. Quienes usen la versión PRG igualmente pueden usar el nuevo seteo "Language: ES" en foxbin2prg.cfg, o cualquiera de los 4 lenguajes soportados (ES, EN, FR, DE)
  • Mejora: Optimización para no reprocesar innecesariamente archivos cuando se usa UseClassPerFile:1
    • En la versión anterior se reprocesaba el vcx/scx tantas veces como archivos file.class.tx2 hubiera. Ahora se optimizó para que solamente lo haga una vez.
  • Nuevo: Agregada la detección de métodos duplicados para notificar casos de corrupción (Álvaro Castrillón)
    • Esta fue una solicitud que por suerte no fue muy compleja de implementar y que va en la línea de lo que considero algo muy útil en un software, que es la posibilidad de diagnosticar y ayudar a resolver problemas, en este caso, de duplicidad de métodos, cosa que puede ocurrir cuando los archivos originales (vcx/scx) están corruptos. Esta corrupción en particular no produce cuelgues en general, lo que la hace más peligrosa, ya que se ejecuta siempre uno de los métodos duplicados, que no necesariamente coincide con lo que se modifique. En un caso extremo puede observarse que se cambia código en un método, pero luego ese código nunca se ejecuta y siguiendo la ejecución con el depurador paso a paso se puede ver como se ejecuta el antiguo código. Ahora esta corrupción queda expuesta y se notifica mediante un log de errores, indicando los números de línea del sc2/vc2 donde se encontraron las duplicidades.



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!

lunes, diciembre 01, 2014

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


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.3 de las herramientas Visual FoxPro 9 para PlasticSCM, con los siguientes cambios:

  • Actualizada la versión de FoxBin2Prg (solo el EXE) a la versión v1.19.37
  • Actualizados los scripts y programas para Plastic para aprovechar las mejoras visuales y funcionales de la nueva versión de FoxBin2Prg




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.37 de FoxBin2Prg (novedades, bug fixes, mejoras... :-)

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.37 de FoxBin2Prg con los siguientes cambios:

  • Nuevo: Separación de clases por archivo (Ryan Harris/Lutz Scheffler/Álvaro Castrillón). Esta ha sido una petición de varias personas, que querían poder trabajar con las clases de una librería VCX por separado, así que ahora se puede. Solo hay que poner UseClassPerFile:1 en el foxbin2prg.cfg.
  • Bug Fix dbf: "Error 1903, String is too long to fit" para DBF_Conversion_Support:4 con DBFs grandes (edyshor). Cuando el tamaño de la tabla a exportar supera cierto límite, aparecía este error. En esta versión se soluciona el problema y se incrementa la velocidad de exportación de forma notable.
  • Bug Fix scx/vcx: Algunas propiedades a veces tomaban la descripción de otras propiedades similares. Cuando hay propiedades cuyo nombre es igual al comienzo de otras (prop -> propiedad) podía pasar que una de las propiedades tome la descripción de la otra.
  • Bug Fix scx/vcx: Las propiedades "Protected" y "Hidden" no siempre estaban ordenadas alfabéticamente. Este ordenamiento alfabético es para minimizar diferencias, aunque inicialmente puede que en varios archivos se vea esto en la próxima conversión.
  • Nuevo: Nuevo parámetro ClearDBFLastUpdate para evitar diferencias por este dato (edyshor). Cuando se abre una tabla, y aunque no se actualice, hay veces que se actualiza el campo interno LastUpdate que se puede consultar con LUPDATE(). Desde ahora esa fecha no se guarda en el db2, y solo se genera en el dbf, en el caso que esté habilitada su regeneración. Esto disminuye bastante las diferencias en los db2.
  • Nuevo: AutoFix para propiedades partidas debido a ediciones manuales del scx/vcx. Debido a ediciones manuales de algunos usuarios, pueden quedar propiedades partidas al medio por un "Enter" sin querer. En esta versión se agrega un algoritmo para volver a unir esas propiedades. Dicho sea se paso, no se deberían modificar los memos de los scx/vcx a mano.
  • Mejora: Las configuraciones de foxbin2prg.cfg no permiten comentarios && al final (edyshor). En las versiones anteriores, si había comentarios && a la derecha del valor, no se reconocía el valor, ahora se reconoce bien aunque haya comentarios &&.
  • Mejora: Agregado control para detectar reportes no compatibles con VFP 9. Ya exitía esta validación para los menús, pero estaba faltando para los reportes, así se puede identificar más fácilmente si tenemos componentes de versiones anteriores sin migrar en VFP 9.
  • Mejora: Agregada cancelación del proceso con tecla Esc. Mientras se están procesando archivos, ahora es posible cancelar la ejecución con la tecla Esc, lo que mostrará un error 1799 de Conversión Cancelada.
  • Mejora: Mejora de los indicadores de proceso para que sean más fluidos. El indicador de proceso ahora tiene 2 barras, una para el avance de los archivos y la otra para el avance interno del archivo.
  • Mejora: Varias optimizaciones para mejorar la velocidad proceso, principalmente en los vcx/scx y DBFs que exporten datos. Para mi asombro he descubierto que hay veces en las que el manejo de archivos a bajo nivel es mucho más rápido que trabajar con variables de memoria, al punto de que una librería VCX que tardaba de 6 a 10 minutos en procesarse, ahora tarda algo más de 2 minutos.



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