Manejo de Errores: Validación de Parámetros

Código 01 elipsificar validando parámetros
String elipsificar(String texto, int longitud) { 
    if (texto == null || texto.isEmpty() || longitud <= 0) { 
      throw new IllegalArgumentException(); 
    }  
    var espacio = texto.indexOf(' ', longitud); 
    var índiceCorte = (espacio != -1) ? espacio : texto.length(); 
    var cortada = texto.substring(0, índiceCorte); 
    return (cortada.length() < texto.length()) ? String.format("%s…", cortada) : texto;
} 

En un ejercicio anterior creamos una función «elipsificar» que acorta un texto y agrega puntos suspensivos, de modo que pueda usarse sin problemas en un diseño gráfico. Pero ¿qué sucede si se utilizan parámetros que no son válidos? En el código 01 tenemos un condicional en la línea 02 que valida el estado de todos los parámetros, y en caso de que alguno sea inválido, se lanza una excepción IllegalArgumentException, concepto que ya habíamos visto al hablar de cómo proteger las invariantes de un tipo de dato. Al lanzar una excepción, salvo casos extraordinarios, el programa terminará.

¿Es esta la mejor manera de manejar argumentos no válidos? Sobre esta cuestión no existe consenso. En este espacio defenderemos la idea de que lanzar una excepción es la mejor opción, y exploraremos sus ventajas y desventajas.

Consideraciones de una estrategia de Manejo de Errores

Antes de hablar sobre una estrategia, es importante preguntarnos ¿qué debemos buscar de una estrategia del manejo de errores en nuestro código?

  1. Debemos procurar que haya errores que sean imposibles
  2. Si hay un error que se pueda evitar esta bien circunnavegarlo
  3. Si ocurre un error, debe ser muy sencillo darse cuenta que lo hubo y muy sencillo encontrar su causa

Los problemas provienen cuando al aplicar el punto 2 rompemos el punto 3. Si bien evitar un error es atractivo, esto no puede ocultar un error que debe ser corregido.

Para el punto «Hacer errores imposibles», esto se logra especialmente creando tipos de datos que protejan sus invariantes. Esto garantiza que si un método recibe un argumento de este tipo, no hay que hacer ciertas validaciones porque es imposible que el objeto no sea válido.

Para el punto «Circunnavegar error si es posible», esto solo es posible en casos muy específicos y por lo tanto no es algo que podamos aplicar frecuentemente. Un ejemplo es un constructor que reciba un String que represente un documento de identidad y se lance un error si el String incluye caracteres que no sean dígitos, por ejemplo que el número sea «123.456.789». Si bien es natural querer guardar el número sin esos caracteres, la realidad es que las personas suelen escribir así sus números de identificación y esa manera de escribirlos de hecho ayuda a evitar errores, entonces ¿por qué debería ser un error? Una manera de escribir ese código, simplificado, sería:

Código 02 NúmeroIdentidad aceptando números con el formato más común
public final class NúmeroIdentidad {    
      
      private final String número;    
      
      public NúmeroIdentidad(String número) {     
        var builder = new StringBuilder();     
        for (var i = 0; i < número.length(); ++i) {       
          var caracter = número.charAt(i);       
          if (Character.isDigit(caracter)) {         
            builder.append(caracter);       
          }     
        }    
          
        this.número = builder.toString();   
      }  
      
      // más código 
    }  

Otro posible ejemplo sería la función sumaRangoNúmeros(int,int) ¿debe el código asumir que «a» siempre es menor que «b»? Una opción es simplemente desde el código asegurarnos de que sea así:

Código 03 sumaRangoNúmeros() circunnavegando que los parámetros vengan en el orden correcto
int sumaRangoNumeros(int a, int b) { 
      var inicio = Math.min(a, b); 
      var fin = Math.max(a, b); 
      
      // fórmula para serie aritmética 
      return (fin - inicio + 1) * (inicio + fin) / 2; 
    }   

Sin embargo estos ejemplos son la excepción y debemos tener mucho cuidado que nuestra intención no termine escondiendo un error más serio. Por ejemplo esté código:

Código 04 elipsificar() validando sus parámetros
String elipsificar(String texto, int longitud) { 
      if (texto == null || texto.isEmpty() || longitud <= 0) { 
        return ""; 
      } 
      
      var espacio = texto.indexOf(' ', longitud); 
      var índiceCorte = (espacio != -1) ? espacio : texto.length(); 
      var cortada = texto.substring(0, índiceCorte); 
      return (cortada.length() < texto.length()) ? String.format("%s…", cortada) : texto; 
    }   

Este es un ejemplo claro de un código que es dañino porque esconde el error al retornar un valor que podría ser válido. En el caso de NúmeroIdentidad el error es que el usuario no solo ingresó números sino otros caracteres y en sumaRangoNúmeros el error es el orden de los parámetros, errores en que es predecible entender qué es lo que quería expresar el cliente del código.

Para eliminar la duda de si es un error o no, preguntémonos:

  • ¿Por qué el texto ha elipsificar podría ser null?
  • ¿Por qué el texto ha elipsificar podría venir vacío?
  • ¿Por qué la longitud del texto esperado podría ser cero o negativa?

Sin una buena respuesta, debemos conceder que todas estás razones son errores.

En la versión inicial al detectar alguno de los errores, el programa lanzaba una excepción. Esto detiene el programa y muestra un error en la pantalla. Es una acción “ruidosa” que hace que sea difícil ignorar el error. Cuando el programa se detiene inmediatamente puedes empezar a seguir el rastro hasta encontrar dónde estuvo el error y corregirlo. Un parámetro que no es válido es siempre un error de código cuya única solución es modificar el código.

En nuestra nueva versión ¿cuándo nos daremos cuenta del error? Puede pasar que no lo hagamos o puede ser que lo hagamos muy tarde. Un error es siempre más difícil de entender cuando no se detecta inmediatamente. Esto es un problema que se puede volver inmanejable en proyectos muy grandes.

¿Pero si es tan buena idea no esconder los errores por qué no es lo que se usa?

El usuario final no debe ver un stacktrace

En los inicios del boom del mundo Web en la primera década del siglo 21 hubo un artículo muy influyente que tenía la siguiente tesis: “El usuario final no debe ver un stacktrace” La hipótesis era simple: un usuario de una página web, al hacer una acción dentro de ella que reciba una página “en blanco” y que el único texto sea un stacktrace es una situación confusa: el stacktrace no le va a indicar qué tipo de error fue, ni si es posible evitarlo si intenta la operación una nueva vez. Más aún no hay manera sencilla de que el usuario “vuelva” a la experiencia de la página web, porque al manejar el error solo vemos el stacktrace. Esto tiene todo el sentido.

El efecto inmediato de este artículo fue que muchas personas adquirieron una fobia a las excepciones porque sintieron que al lanzarlas iban a romper la regla sagrada de “el usuario final no debe ver un stacktrace”.

En programación todas las decisiones son balanceo de ventajas y desventajas.

Empecemos por el extremo: si eres parte de un equipo de desarrollo y uno de los usuarios de una página web reporta que hay una funcionalidad que no hace lo que debe hacer y te envían una captura de pantalla de lo que ellos están viendo, ¿cuál de las dos opciones es mejor: una pantalla con "header" y "footer" y un mensaje de error genérico o un stacktrace? Un stacktrace no parece ser el peor escenario en esa situación.

La respuesta seguramente es: "quisiera tener las dos, una página de error pero al mismo tiempo un lugar donde ver ese stacktrace". La mejor estrategia que se ha desarrollado para esto es:

  • Usar excepciones en nuestro código
  • Tener una funcionalidad en la última “capa” de nuestro software, que se pueda apagar y prender fácilmente dependiendo de en dónde se ejecuta el código, que atrape las excepciones lanzadas por el código y se encargue de manejarlas de modo que: el usuario final no vea un stacktrace, pero el stacktrace pueda ser preservado con un mecanismo de monitoreo.

Este acercamiento depende de las excepciones y por eso es importante acostumbrarnos a lanzar excepciones para validar los parámetros de nuestras funciones.

La solución real: cambios en el código

Algo fundamental que hay que entender es que cuando encuentres un error de validación de parámetros la solución real es hacer un cambio en el código. Si un parámetro no puede ser null, entonces puedes validar que la referencia no lo sea antes de llamar el método. Lo que eso significa es que debes investigar cómo es que esa variable se volvió null en un principio y no culpar la validación.

Todo esto es muy importante de entender para llegar a un punto esencial: en un proceso robusto de desarrollo de software el usuario no debería ver un stack trace como producto de la validación de parámetros porque el error debería ser encontrado por el desarrollador durante la etapa de codificación o por el equipo de QA durante la etapa de pruebas. Y necesitamos que el programa termine con un stack trace para que sea suficientemente ruidoso que para el programador o el equipo de QA sea obvio que sucedió un error y tener información en el stacktrace que al desarrollador le permita encontrar cuál es la raíz del asunto.

El miedo a que un usuario vea un stacktrace por proteger nuestros parámetros es usualmente un miedo a nuestro proceso de desarrollo de software. En la medida que tengamos más herramientas para hacer pruebas efectivamente, este miedo podría ir desapareciendo. Por eso es importante que la funcionalidad en la última capa que finalmente transforma el error en algo más amigable para el usuario sea apagable para que tanto el desarrollador como el equipo de QA pueda experimentar los errores más ruidosamente.

Mensajes de Error

Una excepción lanzada es tan útil como el mensaje de error que la acompañe. Un mensaje como "Hubo un error, por favor intenta más tarde", es una excepción que no tiene utilidad.

Afortunadamente hacer un mensaje de error es relativamente sencillo solo hay 2 cosas a tener en cuenta

  • Detalla muy bien el valor del parámetro que estás validando junto con una explicación de cómo lo esperarías. Por ejemplo “El valor de longitud debería ser positivo, actualmente el valor es -1”. Incluir el valor actual a veces ayuda a dar una primera idea de qué puede estar pasando.
  • No mezcles mensajes de error, un mensaje como “El texto es null o la longitud es negativa” es muy confuso.

Por lo tanto por cada estado a validar debe haber una validación. Veremos la opción ingenua, pero nota que hay una mejor manera:

Código 05 elipsificar() validación diferenciada
String elipsificar(String texto, int longitud) {   
      if (texto == null) {     
        throw new IllegalArgumentException("El parámetro texto no puede ser null");   
      }   
      if (texto.isEmpty()) {     
        throw new IllegalArgumentException("El parámetro texto no puede estar vacío");   
      }   
      if (longitud <= 0) {     
        throw new IllegalArgumentException("El parámetro longitud debe ser positivo. Actual es " + longitud);   
      }    
      var espacio = texto.indexOf(' ', longitud);   
      var índiceCorte = (espacio != -1) ? espacio : texto.length();   
      var cortada = texto.substring(0, índiceCorte);   
      return (cortada.length() < texto.length()) ? String.format("%s…", cortada) : texto; 
    }  

Este código cumple con validar los parámetros pero visualmente se ve un poco denso. Pareciera que la razón principal por la cuál las personas no validan sus parámetros de esta manera es porque agrega mucho ruido al código. Afortunadamente podemos hacerlo mejor.

Librería de Validaciones

La mejor manera de validar los parámetros es creando una librería de validaciones.

De base lo que necesitamos es una nueva clase que sirva como agrupación de funciones para validar, escribamos por ahora:

Código 06 clase Validaciones
public final class Validaciones {    
      
      public static String requiereNoSerVacío(String s, String mensaje) {     
        if (s.isEmpty()) {       
          throw new IllegalArgumentException(mensaje);       
        }      
        
        return s;   
      }    
      public static int requiereSerPositivo(int n, String mensaje) {     
        if (n <= 0) {       
          throw new IllegalArgumentException(mensaje);     
        }      
        
        return n;   
      }  
    
    }   

Podemos hacer todas las funciones que necesitemos de este estilo: validamos una propiedad, y solo una, y en caso de no ser válida lanzamos IllegalArgumentException() usando el mensaje que recibimos como parámetro. Es muy importante que el mensaje venga como parámetro para que el mensaje pueda capturar el contexto del error. Si no lanzamos entonces retornamos el valor. Esto permite que la función se utilice de dos maneras: como una invocación suelta o como parte de una operación.

Una opción sería usar nuestra clase Validaciones así:

Código 07 elipsificar usando la clase Validaciones
String elipsificar(String texto, int longitud) {   
      Objects.requireNonNull(texto, "El texto no puede ser null");   
      Validaciones.requiereNoSerVació(texto, "El texto no puede estar vacío");   
      Validaciones.requiereSerPositivo(longitud, "La longitud debe ser positiva. Valor actual es " + longitud);    
      
      var espacio = texto.indexOf(' ', longitud);   
      var índiceCorte = (espacio != -1) ? espacio : texto.length();
      var cortada = texto.substring(0, índiceCorte);   
      return (cortada.length() < texto.length()) ? String.format("%s…", cortada) : texto; 
    }   

Una opción es hacer validaciones apiladas para hacer un solo llamado a un método pero hacer varias validaciones, cada vez más específicas. Supongamos que tenemos un parámetro que debe ser un valor entre 0 y 255 pero el valor es leído como un String porque proviene de un archivo de configuración.

Código 08 validaciones apiladas
public final class Validaciones {    
      
      public static String requiereNoSerVacío(String s, String mensaje) {     
        Objects.requireNonNull(s, mensaje + ": No puede ser null");     
        if (s.isEmpty()) {       
          throw new IllegalArgumentException(mensaje + ": No puede estar vacío");     
        }      
        
        return s;   
      }    
      
      public static int requiereSerNúmero(String s, String mensaje) {     
        requiereNoSerVacío(s, mensaje);     
        if (!s.matches("-?\\d+")) {       
          throw new IllegalArgumentException(String.format("%s : No es un número entero. Valor actual %s", mensaje, s));     
        }      
        return Integer.parseInt(s);   
      }    
      public static int requiereSerMayorQue(int n, int límiteInferior, String mensaje) {     
        if (n < límiteInferior) {       
          throw new IllegalArgumentException(String.format("%s: Debe ser mayor que %d. Valor actual %d", mensaje, límiteInferior, n));     
        }      
        return n;   
      }    
      public static int requiereSerMenorQue(int n, int límiteSuperior, String mensaje) {     
        if (n < límiteSuperior) { 
          throw new IllegalArgumentException(String.format("%s: Debe ser menor que %d. Valor actual %d", mensaje, límiteSuperior, n));     
        }      
        
        return n;   
      }    
      
      public static int requiereEstarEnRango(int n, int inferior, int superior, String mensaje) {     
        requiereSerMayorQue(n, inferior, mensaje);      
        return requiereSerMenorQue(n, superior, mensaje);   
      }   
      
      public static int requiereValorVálidoParaColor(int n, String mensaje) {     
        return requiereEstarEnRango(n, 0, 255, mensaje);   
      }    
      
      public static int requiereValorVálidoParaColor(String n, String mensaje) {     
        return requiereValorVálidoParaColor(requiereSerNúmero(n, mensaje), 0, 255, mensaje);   
      }  
    }  

Y lo podemos usar así:

var rojo = Validaciones.requiereValorVálidoParaColor(r, "El parámetro r");
var verde = Validaciones.requiereValorVálidoParaColor(g, "El parámetro g");
var azul = Validaciones.requiereValorVálidoParaColor(b, "El parámetro b");

Por este tipo de posibilidades es que usar una librería existente ofrece muy pocas ventajas sobre hacer nuestra propia librería de validaciones.

© 2022 Carlos Obregón. This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0)

Scroll to Top