17 de mayo de 2006

Escribir mejor código (Parte 3)

Artículo original: Writing better code (Part 3)
http://weblogs.foxite.com/andykramek/archive/2006/03/20/1308.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York

En el tercer artículo de esta serie voy a hablar sobre Procedimientos y Funciones. Visual FoxPro, como sus ancestros FoxPro y FoxBase admite dos tipos diferentes de declaración y llamada de código.

Crear un procedimiento

Un procedimiento es sencillamente un bloque de código que es llamado por un nombre. Puede; pero no se requiere, que acepte uno o más parámetros y la razón para la creación de un procedimiento, es evitar la necesidad de escribir el mismo código muchas veces. Los procedimientos son llamados empleando el comando DO, y, de forma predeterminada, todos los parámetros se pasan por referencia. Por tanto no existe la necesidad de tener valores de retorno en los procedimientos - pueden modificar cualquier valor y el código llamado que lo pasa.

He aquí un ejemplo sencillo del tipo de código que podría estar en un procedimiento. Todo lo que hace es aceptar un número de registro y Alias de tabla. Valida que el número de registro es válido en el contexto de una tabla específica y, si es así, mueve el puntero de registro. Si falla algo, o si no es válido, se genera un error:
********************************************************************
*** Nombre.....: GOSAFE
*** Autor...: Andy Kramek & Marcia Akins
*** Fecha.....: 03/20/2006
*** Aviso...: Copyright (c) 2006 Tightline Computers, Inc
*** Compilador.: Visual FoxPro 09.00.0000.3504 for Windows 
*** Función.: Si el registro especificado es válido para el alias 
*** ........: especificado va hasta el, de lo contrario genera un error
********************************************************************
PROCEDURE gosafe
  PARAMETERS tnRecNum, tcAlias
  TRY
    *********************************
    *** Comprobación de los parámetros
    *********************************
    *** Debemos recibir un número de registro!
    IF VARTYPE( tnRecNum ) # "N" OR EMPTY( tnRecNum )
      ERROR "Debe pasar un número de registro a GoSafe" 
    ENDIF
    *** Si no se ha especificado un alias, asume el alias actual
    lcAlias = IIF( VARTYPE( tcAlias ) # "C" OR EMPTY( tcAlias ), ;
      LOWER( ALIAS()), LOWER( ALLTRIM( tcAlias )) )
    IF EMPTY( lcAlias ) OR NOT USED( lcAlias )
      *** No hay tabla!
      ERROR "Debe especificar, o seleccionar una tabla " + ;
        "abierta al llamar a GoSafe"
    ENDIF
    *********************************
    *** Verifica que el número de registro es válido para el alias
    *********************************
    IF BETWEEN( tnRecNum, 1, RECCOUNT( lcAlias ) )
      *** Este está bien
      GOTO (tnRecNum) IN (lcAlias)
    ELSE
      *** No, el número de registro no es bueno
      ERROR "El registro " + TRANSFORM( tnRecNum ) + ;
        " no es válido para la tabla " + lcAlias
    ENDIF 
  CATCH TO loErr
    MESSAGEBOX( loErr.Message, 16, "GoSafe ha fallado" )
  ENDTRY
  RETURN
ENDPROC
Para llamar al procedimiento utilizamos
SET PROCEDURE TO procfile ADDITIVE
*** Guarda el puntero de registro en Account
lcOldAlias = ‘account’
lnOldRec = RECNO( lcOldAlias )
<<más código aquí>>
*** Restaura el puntero de registro
DO GoSafe WITH lnOldRec, lcOldAlias
Este procedimiento en particular no modifica nada y de hecho (como todos los métodos, funciones y procedimientos de VFP) en realidad devuelve .T. No hay necesidad, o incluso posibilidad de capturar ese valor. Vea que si desea pasar valores a un procedimiento por valor, en lugar de por referencia, entonces tenemos que pasarlos como valores reales y no como variables (Si, podríamos incluso cambiar la configuración de UDFPARMS pero no está recomendado, porque tiene otros efectos colaterales no deseados. Además, utilizar una solución global para un tema local es generalmente una mala idea). Entonces, para pasar un registro de número, "por valor" a este procedimiento podríamos utilizar:
DO GoSafe WITH INT( lnOldRec ), lcOldAlias

Crear una función

El otro método para llamar a código es crear una función. La diferencia básica entre un procedimiento y una función es que la función SIEMPRE devuelve un valor y por tanto siempre que llame a una función, ya sea una función nativa o una propia, siempre debe verificar los resultados. Las funciones se llaman finalizándolas con dos paréntesis. Como los procedimientos, las funciones pueden aceptar uno o más parámetros; pero a diferencia de los procedimientos, los parámetros pasados a las funciones son, de forma predeterminada, pasados por valor. La consecuencia es que esta funciones no modifican los valores en el código que los llama. (Si, esto es un comportamiento exactamente opuesto al comportamientos de los procedimientos). Veamos una sencilla función que devuelve el tiempo como una cadena de caracteres después de unos segundos.
********************************************************************
*** Nombre.....: GETTIMEINWORDS
*** Autor...: Andy Kramek & Marcia Akins
*** Fecha.....: 03/20/2006
*** Aviso...: Copyright (c) 2006 Tightline Computers, Inc
*** Compilador.: Visual FoxPro 09.00.0000.3504 for Windows 
*** Función.: Devolver la cantidad de Días/Horas y minutos a 
*** ........: partir de una cantidad en segundos
*** Valor devuelto.: Cadena de caracteres
********************************************************************
FUNCTION GetTimeInWords( tnElapsedSeconds, tlIncludeSeconds )
  LOCAL lcRetval, lnDays, lnHrs, lnMins
  *** Inicializa las variables
  STORE '' TO lcRetval
  STORE 0 TO lnDays, lnHrs, lnMins*** Handle the Days first
  lnDays = INT( tnElapsedSeconds / 86400 )
  IF lnDays > 0
    lcRetVal = PADL( lnDays, 3 ) + ' Days '
  ENDIF
  *** Calcula las horas
  lnHrs = INT(( tnElapsedSeconds % 86400 ) / 3600 )
  IF lnHrs > 0
    lcRetVal = lcRetVal + PADL( lnHrs, 2, '0' ) + ' Hrs '
  ENDIF
  *** Ahora los minutos
  lnMins = INT(( tnElapsedSeconds % 3600 ) / 60 )
  *** Verifica los segundos
  IF tlIncludeSeconds
    *** Si deseamos los segundos, los agrega explícitamente
    lcRetVal = lcRetVal + PADL( lnMins, 2, '0') + ' Min ' 
    lcRetVal = lcRetVal + PADL( INT( tnElapsedSeconds % 60 ), 2, '0' )+' Sec '
  ELSE
    *** Redondea por exceso los minutos UP Si >= 30 segundos
    lnMins = lnMins + IIF( INT( tnElapsedSeconds % 60 ) >= 30, 1, 0 )
    lcRetVal = lcRetVal + PADL( lnMins, 2, '0') + ' Min ' 
  ENDIF
  RETURN lcRetVal
ENDPROC

Como puede ver, a diferencia del procedimiento, este código realmente crea, y devuelve explícitamente una cadena de caracteres a lo que sea que lo llama. (El fundamento detrás de esta función es, por supuesto, que si obtiene la diferencia entre dos valores DateTimes el resultado está en segundos). Entonces, cuando llama a esta función el valor devuelto debe ser controlado de alguna forma. Lo podemos mostrar:
? GetTimeInWords( 587455, .T. ) && Muestra: 6 Días 19 Horas 10 Min 55 Seg
o lo guarda como variable:
lcTime = GetTimeInWords( 587455 )
o en el portapapeles (para pegarlo en alguna otra aplicación, por ejemplo)
_cliptext = GetTimeInWords( 587455 )
Recuerde que el comportamiento predeterminado de Visual Foxpro es que los parámetros se pasan por valor a una función para que si necesita pasar los valores por referencia (y esto es lo que surge más comúnmente al tratar con matrices) entonces, debe pasar la referencia precedida por un signo "@&quot;, de esta forma:
DIMENSION gaMyArray[3,2]
luRetVal = SomeFunction( @gaMyArray )

Entonces, ¿cuál es la diferencia?

Aunque yo declaro este código como una FUNCTION y el código GoSafe como un PROCEDURE, en la práctica a VFP no le importa. Podemos llamar código que está declarado como un procedimiento como si fuera una función (y vice-versa). Así podemos llamar al procedimiento "GoSafe" referido antes, de esta forma:
llStatus = GoSafe( lnOldRec, lcOldAlias )
El valor devuelto en llStatus podría, dada la forma en que está escrito el procedimiento SIEMPRE ser .T.; pero si modificamos el código sólo un poco, tendríamos el valor devuelto sea o no exitoso el procedimiento al colocar el puntero del registro correctamente. La única modificación necesaria es verificar el éxito después de mover el puntero del registro, de esta forma:
IF BETWEEN( tnRecNum, 1, RECCOUNT( lcAlias ) )
  *** Esto está OK
  GOTO (tnRecNum) IN (lcAlias)
  *** Verifica que tenemos el registro correcto
  *** Si está bien, da valor de retorno = .T.
  STORE ( RECNO( lcAlias ) = tnRecNum ) TO llRetVal
ELSE
  *** No, el puntero de registro está mal
  ERROR "registro " + TRANSFORM( tnRecNum ) + ;
    " no es válido para la tabla " + lcAlias
ENDIF 
CATCH TO loErr
  MESSAGEBOX( loErr.Message, 16, "GoSafe Failed" )
  *** Fuerza el valor devuelto a .F. en cualquier caso de error
  llRetVal = .F.
ENDTRY
*** Devuelve el valor
RETURN llRetVal
Por supuesto podríamos ejecutar la función GetTimeInWords llamándola como procedimiento, de esta forma:
DO GetTimeInWords WITH 587455, .T.
Pero no haríamos muy bien, porque la función no hace nada aparte de devolver un valor - el que en este caso no podemos detectar. Lo interesante de todo esto es:
  • Primero, VFP en realidad no le importa si se ha escrito como PROCEDURE o como FUNCTION. Solamente es código para ser ejecutado. ¿Qué importancia tiene si el código es llamado como procedimiento (empleando DO gosafe) o como función (llStatus = gosafe())?
  • Segundo, si está llamando una función es vitalmente importante verificar el valor devuelto. Mientras los procedimientos usualmente encierran su propio control de errores (como el ejemplo que se ha brindado), el valor devuelto por una función es usualmente la única indicación de que realizó o no lo que se esperaba. Esto es válido si la función en cuestión está definida en un archivo de procedimiento, como un programa stand-alone, un método en un objeto o una función Visual FoxPro nativa.
¿Qué tiene todo esto que ver con escribir mejor código?

Ahora, puede pensar que todo esto es muy básico, y obviamente, porqué yo le molesto con esto. Pero usted sabe, una de las cosas más comunes que he visto en varios foros es un mensaje que dice algo como:

“TableUpdate no funciona”

Aparte del comentario obvio, de que no hay un error en la operación ya que es una función TableUpdate() que ha sido verificada para la función actual de VFP, esto significa realmente que "No puedo hacer que trabaje TableUpdate". Generalmente la función viene junto con estas líneas:

"Cuando los usuarios cambian los datos, sus cambios son visibles en el formulario; pero no son guardados en la base de datos. No hay un error; pero la tabla no se actualiza. Ayúdenme por favor."

Y cuando la persona publica su código (frecuentemente, después de habérselo pedido muchas veces - ¿por qué a algunas personas no les gusta publicar su código?) nos encontramos esto:
=TableUpdate( .T. )
Ahora, esta única línea de código, que existe en todas las versiones del archivo de ayuda de VFP (si, incluyendo VFP 9.0) creo que es responsable del mayor gasto de tiempo del desarrollador, que cualquier otra cosa que se haya escrito ¿Qué está mal aquí? Veamos:

Primero, TableUpdate() es una función, la que, como hemos podido ver, significa que debe devolver un valor. ¿Dónde se verifica este valor? Incluso el archivo de ayuda dice claramente que se devuelve un valor, no indica en ningún ejemplo del archivo de Ayuda que necesita verificarlo. Sin embargo, está claro, cuando lee la letra pequeña, porqué es imperativo verificar el valor devuelto:
Valores devueltos
Lógico. TABLEUPDATE( ) devuelve verdadero (.T.) si se confirman los cambios realizados en todos los registros; de lo contrario, TABLEUPDATE( )devuelve falso (.F.).
¿Qué significa esto? Simplemente que si falla una llamada a TableUpdate() al actualizar un registro, la única forma en que lo puede saber es verificando el valor devuelto.

Segundo, se pasa solamente uno de los CUATRO parámetros posibles para TableUpdate(), ni siquiera el nombre de la tabla que se supone que sea actualizada.

Tercero, utiliza el viejo (VFP 3.0, VFP 5.0) valores lógicos para el primer parámetro, independientemente del hecho que la posibilidad de utilizar funcionalidad extendida se introdujo en la versión 6.0 De hecho, ¡ la única cosa cierta en este ejemplo es la ortografía de la palabra "TableUpdate"!

¿Cómo debe verse este ejemplo? Pues así:
*** Hubo cambios solamente en el registro actual
llOk = TABLEUPDATE( 1, .F., 'employee' ) 
IF llOk
  *** TableUpdate Exitoso
  ? 'Actualizado el valor cLastName: '
  ?? cLastName && Muestra el  valor atual cLastName (Jones)
ELSE
  *** Falló TableUpdate - ¿POR QUÉ?
  AERROR( laErr )
  MESSAGEBOX( laErr[ 1, 2], 16, 'Falló TableUpdate ' )
ENDIF
Para ver más detalles del uso de TableUpdate() (y TableRevert()) vea mi artículo del blog "Handling buffered data in Visual FoxPro"  de Diciembre de 2005.

Nota de la traductora: El artículo referido, será traducido al español y publicado como: Controlar datos en buffer Visual FoxPro

Conclusión

El punto aquí es que debe tener el hábito, si aun no lo tiene, de verificar los valores devueltos de llamadas de funciones - incluso cuando SEPA de antemano (¿cuántas veces tiene un comando SEEK y luego no verifica el resultado? ¡Sea honesto!)

La única excepción posible que puedo pensar, podría ser TableRevert() porque no estoy seguro de cómo podría fallar, o incluso que podría usted hacer si falla. Parece más un comando o procedimiento, que una verdadera función, por supuesto, devuelve un valor (por ejemplo, la cantidad de registros revertidos).

No hay comentarios. :

Publicar un comentario