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