miércoles, 4 de abril de 2018

Control de errores - Clean Code

El tratamiento de los errores es otra tarea con la que los programadores han de lidiar de una manera habitual. Un programador se puede encontrar con problemas como: los recursos no están disponibles, la base de datos falla, etc. El control de errores es importante siempre y cuando no dificulte la comprensión de la intención de la lógica del negocio.

Uso de excepciones en lugar de código de retorno


Los lenguajes de programación que no disponen de excepciones tienden a utilizar otras estrategias como, por ejemplo, devolver códigos de error o presentar indicadores de error («error flags»).

El problema con los código de error que se devuelven en el retorno de una función es que obligan a tratar el error en ese mismo instante, de manera que complica la lógica del código que ha realizado la llamada.


Con esta estrategia se puede apreciar como el control de los distintos errores pueden complicar la lógica ya que con cada control de error se produce una indentación del código. Si el lenguaje de programación tiene excepciones se puede optar por lanzar una excepción cuando se encuentra un error.

En el siguiente código se puede ver cómo se podrían lanzar las distintas excepciones.


En este caso el código queda más limpio. No es simplemente una cuestión de estética sino que los puntos clave están separados en distintas funciones independientes entre sí.

Escribir el código try-catch-finally primero


Los bloques de código try-catch definen una zona de visibilidad o alcance para las variables. La ejecución del código puede pararse en cualquier momento y continuar en el bloque catch.

Desde esta perspectiva los bloques try-catch son como transacciones de manera que el bloque catch deja el sistema en un estado consistente independientemente de lo que haya sucedido en el try. Por esta razón es una buena práctica escribir el bloque try-catch-finally primero si el código pudiera lanzar alguna excepción.

Uso de excepciones sin comprobar


Las excepciones comprobadas o «checked exception» son aquellas excepciones que se controlan en la propia función o que en la declaración del método definen que pueden lanzar esta excepción.

La desventaja de usar este tipo de excepciones es que estas vulneran el OCP (Open-Close Principle). Si se lanza una excepción y, esta es controlada tres niveles por encima, se tienen que declarar en cada método entre el lanzamiento de la misma y el catch que lo controla, es decir, los tres métodos en este caso. Esto significa que un cambio en un nivel inferior puede forzar a modificar varios niveles superiores.

Se rompe la encapsulación ya que los niveles superiores no deben saber sobre los detalles de implementación de estas excepciones de más bajo nivel. En general, los costes de utilizar este tipo de excepciones superan los beneficios que conllevan.

Dar contexto junto con las excepciones


Es importante dar la suficiente información para poder determinar la operación que falló, dónde se lanzó la misma y qué fallo se produjo, de manera que resulte más fácil saber qué está pasando.

Definir los tipos de excepciones en función de las necesidades de quién las usa


La organización de las excepciones se puede hacer de diferentes maneras. Se pueden clasificar por su fuente, desde qué componente son lanzadas, según el tipo de error, si son errores de algún dispositivo, de red, si son errores de tiempo de ejecución, etc. Sin embargo, cuando se definen excepciones, la organización debería centrarse en cómo se van a controlar.

Siguiendo con el caso anterior acerca de mover un archivo.


En este caso se puede apreciar el código duplicado en cada catch. Dada la sencillez del código, este se puede simplificar aún más extrayendo una nueva clase, un «wrapper», que abstraiga la funcionalidad de mover una imagen, devolviendo una excepción que englobe las anteriores.

La clase ImageFile simplemente abstrae la funcionalidad.


Este tipo de estrategias es muy útil ya que, en realidad, se está envolviendo la API del sistema para mover un archivo y, como se ha visto anteriormente, es una buena práctica ya que esto minimiza las dependencias. Además, en un futuro se podría mover el archivo a otra localización que no sea el propio servidor sino a otro servidor a través de un FTP, una CDN a través de una API REST, etc. lo que el cambio implicaría crear otro colaborador en el que recaiga esta responsabilidad.

Definir el flujo normal


Siguiendo las directrices anteriores, el código que se desarrolle tendrá una buena separación entre la lógica y el control de errores con lo que la mayor parte de este tenderá a ser una descripción del algoritmo.

Habrá algunas ocasiones en las que haya parte de lógica en una excepción.

En este caso se está calculando el valor total de los post de un usuario pero si dicho usuario no tiene publicaciones, entonces se le aplica un valor por defecto. Sería mucho más claro si esa parte de la lógica no estuviera incluida en el catch de manera que «postsReport» siempre devolviese un objeto de tipo «ReportPerUser» y fuera este quien devolviese el valor por defecto en el caso de no encontrar publicaciones.

Fowler llama a esta estrategia «Special Case Pattern», la cual consiste en crear una clase y configurar un objeto para que se encargue del caso especial. El comportamiento se encapsula en el objeto de este caso especial.

No devolver null


Los valores nulos necesitan de un control y, si este no es el adecuado, se puede dar una interrupción en tiempo de ejecución. Se considera una buena práctica devolver una excepción o una Caso Especial («Special Case Pattern») en vez de un valor nulo.

En el siguiente ejemplo se puede observar cómo en el caso de que la función «getPosts» devuelva un valor nulo, resulta obligatorio hacer la comprobación.


Si el control del valor nulo no estuviera implementado podría llegar a dar un error en tiempo de ejecución por lo que, en cualquiera de los dos casos, el código se puede mejorar.

En los casos en los que se espera una lista se puede utilizar la estrategia de devolver una lista vacía. Esta lista vacía no produciría un error al tratar de iterar sobre sus elementos. En este caso habría que cambiar la implementación de «getPosts()» para que devuelva una lista vacía y se podría evitar el control del valor nulo.

No pasar valores nulos


Devolver valores nulos es una mala práctica pero pasar un valor nulo a un método es peor aún. Salvo en los casos en los que se trabaje con una API la cual espere un parámetro con el valor nulo, se debe evitar siempre que sea posible.

Pasar un parámetro con un posible valor nulo obliga a cada función a validar los datos de entrada mediante un control de errores y a lanzar una posible excepción. La única manera de evitar este control es, en algunos lenguajes de programación, prohibir al parámetro pasar un nulo por defecto.

Conclusión


El código limpio debe ser legible y robusto. Un código robusto es aquel que está preparado ante los posibles errores que se pueden producir, pero este control de errores debería hacerse independiente de la lógica del negocio haciendo el código más legible.

No hay comentarios:

Publicar un comentario