19 de septiembre de 2017

El dilema de controlar una excepción en Visual FoxPro

Artículo original: Exception Handling Dilemma in VFP
http://west-wind.com/weblog/posts/4538.aspx
Autor: Rick Strahl
Traducido por: Ana María Bisbé York


Me ha llamado la atención, hace un par de días, que cambiando el mecanismo de control de errores de Web Connection por uno nuevo basado en TRY/CATCH, en lugar del método tradicional de controlar el error, que tiene mucha menos funcionalidad.

En Web Connection 4.0 todas las clases para procesar errores se controlan en el método Error en la clase Process la que básicamente captura todos los errores no controlados. El problema con el método de error es que no puede inhabilitarlo fácilmente, entonces, para tener un entorno de desarrollo donde los errores no sean controlados y el entorno de ejecución donde están tengo que utilizar un código como este:

*** Este bloque anula el método Error para que pueda tener los errores en
*** tiempo de depuración interactiva mientras ejecuta dentro de VFP.
#IF !DEBUGMODE
**************************************
FUNCTION Error(nError, cMethod, nLine)
**************************************
  LOCAL lcLogString, llOldLogging, lcOldError
  *** ¿Hemos sobrecargado la pila de llamadas? Si es así, nos vamos
  IF PROGLEVEL() > 125
    RETURN TO PROCESSHIT
  ENDIF
  nError = IIF(VARTYPE(nError) = "N", nError, 0)
  cMethod = IIF(VARTYPE(cMethod) = "C", cMethod, "")
  nLine = IIF(VARTYPE(nLine) = "N", nLine, 0)
  *** Guardamos el valor actual
  lcOldError = ON("ERROR")
  ON ERROR *
  *** Cerramos esta petición con una página de error 
  *** - SAFE MESSAGE (no confiar en ningún objeto)
  This.ErrorMsg("Ha ocurrido un error..",;
    "En este momento no se puede servir esta petición " + ;
    "debido a dificultades técnicas.<P>" + ;
    "No. Error: " + STR(nError) + "<BR>" + CRLF + ;
    "Error: " + MESSAGE()+ "<P>" + CRLF + ;
    "Método: " + cMethod + "<BR>" + CRLF + ;
    "Código actual: "+ MESSAGE(1) + "<BR>" + CRLF + ;
    "Línea de código actual: " + STR(nLine) + "<p>" + ;
    "Excepción controlada por " + This.CLASS + ".Error()")
  *** Obriga a cerrar el archivo y es recuperable por wc.dll/exe
  *** NOTA: Los objetos HTML no se liberan aquí debido a otra
  *** referencia de objeto como Response.
  IF TYPE("This.oResponse") = "O" AND !ISNULL(This.oResponse)
    This.oResponse.DESTROY()
  ENDIF
  * wait window MESSAGE() + CRLF + MESSAGE(1) + ;
  *   CRLF + "Método: " + cMethod nowait
  IF TYPE("This.oServer") = "O" AND !ISNULL(This.oServer)
    *** Intenta obtener un log del error - Fuerza el log!!!
    * llOldLogging = This.oServer.GetLogToFile()
    llOldLogging = This.oServer.lLogToFile
    lcLogString = "Procesando Error - " + ;
      This.oServer.oRequest.GetCurrentUrl() + ;
      CRLF + CRLF + "<PRE>" + CRLF + ;
      " Error: " + STR(nError) + CRLF + ;
      " Mensaje: " + MESSAGE() + CRLF + ;
      " Código: " + MESSAGE(2) + CRLF + ;
      " Programa: " + cMethod + CRLF + ;
      " Línea No: " + STR(nLine) + CRLF + ;
      " Cliente: " + This.oRequest.GetIpAddress() + CRLF + ;
      "Post Buffer: " + This.oRequest.cFormVars + CRLF + ;
      "</PRE>" + CRLF + ;
      "Excepción controlada por: " + This.CLASS+".Error()"
    This.oServer.LogRequest(lcLogString,"Local",0,.T.)
    This.oServer.SetLogging(llOldLogging)
    This.SendErrorEmail("Web Connection Error - " + ;
      This.oServer.oRequest.GetCurrentUrl(), lcLogString)
  ENDIF
  ON ERROR &lcOldError
  *** Regresa al método Process!
  RETURN TO ROUTEREQUEST
ENDFUNC
* EOF wwProcess::Error
#ENDIF 

Ahora está trabajando bien; pero siempre ha sido un esquema que huele mal. El primer punto es que existe un requerimiento del compilador para habilitar y deshabilitar el interruptor para el modo debug. Entonces, necesita ser recompilado para hacer que cambie entre los dos.

El otro problema más delicado, es que confía en RETURN TO para devolver a un método especificado que lo haya llamado, lo que no es siempre confiable. De hecho, si en algún lugar en la cadena de llamada causa error una ejecución EVAL() el mecanismo de error entero se rompe porque VFP 8 y 9 no admiten RETURN TO en llamadas EVALUATE(). Cuando este es el caso, RETURN TO hace return sencillo y usted termina la ejecución del código SIGUIENDO el error.

Nunca me había gustado el método Error en estos casos porque no existe una forma determinista para devolverlo a algún lugar, entonces VFP 8 llegó con algo que me gustó mucho ver: TRY/CATCH y la capacidad de tener más control determinista del error que permite volver a un lugar específico del código.

Entonces, con Web Connection 5.0 el método Error y el proceder DEBUGMODE (el que de paso, es aplicado también a otras clases) fue sustituido por un controlador TRY/CATCH como centro del motor de proceso. He aquí la implementación que no tiene ningún método de error especial; pero en su lugar, el lo llama:

************************
FUNCTION Process()
************************
  LOCAL loException
  PRIVATE Response, REQUEST, Server, Session, Process
  Process = THIS
  Server = This.oServer
  Request = This.oRequest
  Response = This.oResponse
  Session = This.oSession
  Config = This.oConfig
  *** Método gancho
  IF !This.OnProcessInit()
    RETURN
  ENDIF
  IF Server.lDebugMode
    This.RouteRequest()
  ELSE
    TRY
      This.RouteRequest()
    CATCH TO loException
      This.OnError(loException)
    ENDTRY
  ENDIF
  This.OnProcessComplete()
  RETURN .T.
ENDFUNC
* EOF wwProcess::Process 

El controlador TRY/CATCH captura cualquier error y entonces, sencillamente llama a un método sobreescribible que es utilizado para controlar el error. Un controlador predeterminado es proporcionado o el desarrollador puede sustituir OnError para hacer cualquier cosa que necesite. Desde la perspectiva del diseñador esto está mucho más claro y no tiene un error potencial utilizando RETURN TO.

Pero, existe desafortunadamente, una funcionalidad perdida. TRY/CATCH está bien; pero su empleo termina con algunas limitaciones. Este controlador:

  • No nos brinda una información detallada del error
  • No tenemos la pila de llamada - el control se devuelve al nivel que lo llama.

CATCH permite recibir un objeto exception; pero desafortunadamente este objeto no brinda mucha información y la información disponible no está completa como en el método Error. La razón principal para esto es que cuando ocurre una operación CATCH limpia la pila de llamada con lo que lo coloca en el método inicial que ha realizado la llamada. Esto significa que puede obtener información detallada del error desde el nivel de la pila de llamadas donde, realmente ocurra el error. Cualquier variable, PRIVATE o LOCAL se habrá perdido y ASTACKINFO() solamente devolverá la pila actual, la que en realidad no se corresponde con callstack de el lugar donde ocurrió el error.

Esto significa además, que su información de error está limitada por lo que provee el objeto Exception y no es tan completa como lo que brinda LINENO(), PROCEDURE y SYS(16). El resultado es que en muchas situaciones puede obtener LineNo y ejecutar el nombre del programa de la forma en que tiene en el método Error, especialmente en tiempo de ejecución sin tener información de depuración.

Alternativas? Realmente no ...

Entonces, estuve intentando obtener algunas soluciones para esta limitación - desafortunadamente no he encontrado la forma correcta de hacerlo. Mi primera idea fue hacer que funcione el método Error además del controlador TRY/CATCH. Un par de ideas que vinieron a mi mente:

  • Sobreescribir Try/Catch con el método Error
  • Utilizar un método Error junto al Try/Catch y emplear THROW para crear una excepción personalizada
FUNCTION Error(lnerror,lcMethod,lnLine)
  *** Aquí su controlador de error personalizado
  RETURN TO RouteRequest
ENDFUNC

Desafortunadamente esto no va a funcionar: VFP no permitirá RETURN TO ni RETRY dentro de un bloque TRY/CATCH. Denegado.

La segunda opción tendría un proceder similar; pero en su lugar de devuelve la información del error capturado y luego crea una excepción personalizada que contenga información adicional:

FUNCTION Error(lnerror,lcMethod,lnLine)
  This.StandardPage("Error Ocurrido","Prueba")
  LOCAL loException as Exception
  loException = CREATEOBJECT("Exception")
  loException.LineNo = lnLine
  loException.ErrorNo = lnError
  loException.Message = MESSAGE()
  THROW loException
ENDFUNC

Desafortunadamente, esto tampoco funciona el error arrojado en el método Error es arrojado a su vez, al nuevo controlador de errores. Lo único que puede controlar una excepción en el método Error es ON ERROR. Por tanto, esto tampoco funciona.

Próxima idea: Capturar la excepción y adjuntarla en una propiedad, para que la información adicional esté disponible. Entonces, sencillamente devolvemos el Error del método Error. Desafortunadamente esto tampoco funciona, porque no se puede acortar el circuito de procesamiento del error - Cualquier código siguiente al error original continúa ejecutándose.

Entonces, al final está claro que el método Error() que necesita mostrar la cadena de error no coexiste de manera limpia con el controlador TRY/CATCH. No veo al forma que puedo hacer que esto funcione utilizando un proceder donde se empleen ambos .

Al final, mi solución es agregar un indicador lUseErrorMethodErrorHandling a la clase que procesa y luego, basarme en el salto de la llamada TRY/CATCH

IF Server.lDebugMode OR ;
  This.lUseErrorMethodErrorHandling
  This.RouteRequest()
ELSE
  TRY
    This.RouteRequest()
  CATCH TO loException
    This.OnError(loException)
  ENDTRY
ENDIF

Entonces, el controlador de error funciona como en la versión vieja de Web Connection, excepto en que el desarrollador es responsable por hacer su propio controlador de error. Para simular un comportamiento similar y para componer el mensaje de error como TRY/CATCH puede hacer lo siguiente:

FUNCTION Error(lnerror,lcMethod,lnLine)
  LOCAL loException as Exception
  loException = CREATEOBJECT("Exception")
  loException.LineNo = lnLine
  loException.LineContents = MESSAGE(1)
  loException.ErrorNo = lnError
  loException.Message = MESSAGE()
  loException.Procedure = SYS(16)
  This.OnError(loException)
  RETURN TO RouteRequest
ENDFUNC

Esto funciona; pero no es lo que yo considero la implementación más limpia. Sería muy bueno que VFP nos permitiera alguna opción para tener un gancho que pueda controlarse cuando es creado un objeto Exception. En lugar de pasar una referencia a un objeto Exception podría pasar un tipo - VFP podría instanciar el tipo y usted podría sobreescribir, digamos el Init() de ese tipo para reunir toda la información relevante de la pila de llamadas hasta ese punto. O tener la referencia que tenga un método que sea llamado en la inicialización. O al menos, si TRY/CATCH admite THROW para encadenar el método error.

Bueno, esto funciona aunque tiene truco...


No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.