try-catch – c# ¿Cuándo validar o cuándo lanzar una excepción?

Cuando lanzar una excepción
Cuando lanzar una excepción

Artículos relacionados:

Hablar de cuando validar o cuando lanzar una excepción no es nada fácil, y hasta creo que es un tema controvertido, por eso encontramos muchos problemas al momento de revisar código. A veces se usan las excepciones para manejar la lógica del programa debido a que no se tiene claro que significa manejar la lógica a punta de catch, pero verán que es algo muy frecuente y ocurre sin que muchos se den cuenta. Una excepción es un comportamiento no esperado, un error que ocurre con poca frecuencia, OJO, durante la ejecución de un programa, un motivo puede ser que un método recibió un dato que no  esperaba y por lo tanto no puede ejecutarse con normalidad, ejemplo: para la suma de 2 números positivos se envía un numero negativo, etc . Lo recomendado, según el caso, es primero validar antes de ejecutar algo, recuerden que lanzar excepciones de manera irresponsable tiene su pegada en el rendimiento. Veamos a continuación el siguiente caso:

1. Escenario

Para esta parte tenemos la siguiente historia de usuario:

Historia de usuario: Yo como vendedor necesito registrar un pedido de productos en oferta para vender los productos que quedaron en el almacén.

Criterios de aceptación:

  1. Monto de venta mayor a 1000: Cuando el monto de la venta es > 1000 se debe mostrar el mensaje “El monto del pedido debe ser menor a 1000”.
  2. Cantidad de productos mayor a 3: Cuando la cantidad de productos es > 3 se debe mostrar el mensaje “La cantidad de elementos debe ser menor a 3”.
  3. Cliente con deuda pendiente: Cuando el usuario tenga una deuda pendiente no se le debe registrar un pedido se debe mostrar el mensaje “El usuario tiene una deuda pendiente“.
  4. Pedido registrado: Cuando el monto de venta es < 1000, la cantidad de productos es  < 3 y el cliente no tiene deudas se debe registrar el pedido en la base de datos.

2. Conceptos a tener en cuenta

2.1 Cosas que no se deben hacer

Debemos tener en cuenta que la validación de la cantidad de productos, la deuda pendiente del cliente y el monto total de la venta ya fueron especificados, son casos que van a pasar y son conocidos, por tal motivo no deben tomarse como excepciones. Veamos estos dos casos:

Usar el bloque catch para validar flujos dentro del mismo método
public string Registrar(Pedido pedido)
{
    try
    {
        var cliente = 
            repositorioCliente.BuscarPorCodigo(pedido.CodigoCliente);
        if (cliente == null) throw new ClienteNoExisteException("El cliente no existe");
        //codigo
        if (pedido.CantidadProductos > 3)
        {
            throw new CantidadProductosException
               ("La cantidad de elementos debe ser menor a 3");
        }
        //codigo
    }
    catch (ClienteNoExisteException clienteNoExisteException)
    {
        //codigo
        return clienteNoExisteException.Message;
    }
    catch (CantidadProductosException cantidadProductosException)
    {
        //codigo
        return cantidadProductosException.Message;
    }
    //Codigo
}
Usar el bloque catch para validar flujos desde otro método
public string Registrar(Pedido pedido)
{
    var cliente = repositorioCliente.BuscarPorCodigo(pedido.CodigoCliente);
    if (cliente == null) throw new ClienteNoExisteException("El cliente no existe");
    //codigo
    if (pedido.CantidadProductos > 3)
    {
        throw new CantidadProductosException("La cantidad de elementos debe ser menor a 3");
    }
}

[HttpPost]
public string RegistrarPedido(Pedido pedido)
{
    try
    {
        var servicioPedido = new ServicioPedidoOferta();
        servicioPedido.Registrar(pedido);
    }
    catch (ClienteNoExisteException clienteNoExisteException)
    {
        //codigo
        return clienteNoExisteException.Message;
    }
    catch (CantidadProductosException cantidadProductosException)
    {
        //codigo
        return cantidadProductosException.Message;
    }
}

2.1 Cosas que se deben hacer

Debemos tener en cuenta los criterios de aceptación especificados, ya se conocen ciertas validaciones que se tiene que hacer en el código y por nada en el mundo deben ser tratadas como excepciones, ya que no son comportamientos raros o inesperados en el sistema. Recuerden, si algo se sabe que va a suceder no puede ser tomado como un error o una excepción para eso primero se tiene que validar antes de ejecutar el código. Veamos lo siguientes ejemplos:

Validar antes devolviendo un flag

Se debe usar esta forma de validar en el caso que no importe saber que validación no se cumplió.

public class ServicioPedidoOferta
{
    private bool _esValido;

    public bool Validar(Pedido pedido)
    {
        //Codigo
        return _esValido;
    }

    public void Registrar(Pedido pedido)
    {
        //Codigo
    }
}

[HttpPost]
public string RegistrarPedido(Pedido pedido)
{
    var servicioPedido = new ServicioPedidoOferta();
    if (servicioPedido.Validar(pedido))
    {
        servicioPedido.Registrar(pedido);
    }
    else
    {
        return "Los datos ingresados no son validos";
    }
}
Validar antes devolviendo un enumerador

Si necesitamos tomar una acción dependiendo del resultado de la validación se debe usar un enumerador:

public enum TipoRespuestaValidacion
{
    CantidadProductosMayor,
    MontoTotalMayor,
    ClienteConDeuda,
    Exito
}
public class ServicioPedidoOferta
{
    private TipoRespuestaValidacion tipoRespuestaValidacion;
    public TipoRespuestaValidacion Validar(Pedido pedido)
    {
        tipoRespuestaValidacion = TipoRespuestaValidacion.Exito;
        if (pedido.CantidadProductos > 3)
        {
            tipoRespuestaValidacion = TipoRespuestaValidacion.CantidadProductosMayor;
        }

        if (pedido.MontoTotal > 1000)
        {
            tipoRespuestaValidacion = TipoRespuestaValidacion.MontoTotalMayor;
        }
        //Codigo
        return tipoRespuestaValidacion;
    }    

    public void Registrar(Pedido pedido)
    {
        //Codigo
    }
}

[HttpPost]
public IEnumerable<string> RegistrarPedido(Pedido pedido)
{
    var servicioPedido = new ServicioPedidoOferta();
    var resultadoValidacion = servicioPedido.Validar(pedido);

    switch (resultadoValidacion)
    {
        case TipoRespuestaValidacion.Exito:
            servicioPedido.Registrar(pedido);
            break;
        case TipoRespuestaValidacion.CantidadProductosMayor:
            //Codigo
            break;
        case TipoRespuestaValidacion.ClienteConDeuda:
            //Codigo
            break;
        case TipoRespuestaValidacion.MontoTotalMayor:
            //Codigo
            break;
    }
}
Validar antes devolviendo una lista de mensajes

Se debe usar esta forma de validar en el caso que si importe saber que validaciones no se cumplieron.

public class ResultadoValidacion
{
    public IEnumerable<string> Mensajes { get; private set; }

    public ResultadoValidacion(IEnumerable<string> mensajes)
    {
        Mensajes = mensajes;
    }

    public bool EsValido()
    {
        return !Mensajes.Any();
    }
}

public class ServicioPedidoOferta
{
    private ResultadoValidacion _resultadoValidacion;

    public ResultadoValidacion Validar(Pedido pedido)
    {
        //Codigo
        return new ResultadoValidacion(ObtenerReglasNoCumplidas(pedido));
    }
    public void Registrar(Pedido pedido)
    {
        //Codigo
    }
    private IEnumerable<string> ObtenerReglasNoCumplidas(Pedido pedido)
    {
        if (pedido.CantidadProductos > 3)
        {
            yield return "La cantidad de elementos debe ser menor a 3";
        }

        if (pedido.MontoTotal > 1000)
        {
            yield return "El monto del pedido debe ser menor a 1000";
        }
    }
}

[HttpPost]
public IEnumerable<string> RegistrarPedido(Pedido pedido)
{
    var servicioPedido = new ServicioPedidoOferta();
    var resultadoValidacion = servicioPedido.Validar(pedido);
    if (resultadoValidacion.EsValido())
    {
        servicioPedido.Registrar(pedido);
    }
    else
    {
        return resultadoValidacion.Mensajes;
    }
}
Verificar los parámetros de entrada de un método

Algo para mencionar y dejar claro, una cosa son las validaciones que se definieron en los criterios de aceptación y otra cosa son las validaciones que toda clase o método debe hacer para asegurarse que se ejecute correctamente. Otra cosa que debemos tener en cuenta es el Fail-Fast.

public void Registrar(Pedido pedido)
{
    var cliente = _repositorioCliente.BuscarPorCodigo(pedido.CodigoCliente);
    if(cliente == null)
    {
       throw new Exception("El cliente no existe.");
    }        
    //codigo
}

¿Por qué esta validación es tratada como una excepción?

Respuesta: Si el método Registrar recibe un código de cliente que no existe significa que hay un bug en la aplicación, esto puede ocurrir porque no se están capturando o almacenando adecuadamente los códigos o algo está fallando en el código. Esto es considerado como un escenario no contemplado, por lo tanto el método no puede funcionar correctamente si no existe el cliente. Recuerden este error no debe ocurrir siempre es algo temporal hasta que corrijan el código.

¿Qué pasa si en producción comienza a ocurrir este error?

Respuesta: Eso indica que no se probó adecuadamente el código o no se contemplaron todos los escenarios necesarios. Este error es algo temporal significa que no siempre se va a lanzar esta excepción, recuerden que de esta forma el método se protege, no debe existir miedo de usar excepciones siempre y cuando se hagan de la forma correcta. En caso contrario, si no validamos que la variable no sea nula nuestro código va a lanzar una excepción del tipo NullreferenceException, lo malo de este error es que no nos da mucho detalle de lo que en verdad ocurrió.

3. Diseño de la solución

public class ResultadoValidacion
{
    public IEnumerable<string> Mensajes { get; private set; }
    public bool EsValido { get; private set; } 

    public ResultadoValidacion(IEnumerable<string> mensajes)
    {
        Mensajes = mensajes;
        EsValido = !mensajes.Any();
    }
}
public class ServicioPedidoOferta
{
    private ResultadoValidacion _resultadoValidacion;

    public ServicioPedidoOferta()
    {
        _resultadoValidacion = new ResultadoValidacion();
    }

    public ResultadoValidacion Validar(Pedido pedido)
    {
        //Codigo
        _resultadoValidacion = new ResultadoValidacion(ObtenerReglasNoCumplidas(pedido));
        return _resultadoValidacion;
    }
    public void Registrar(Pedido pedido)
    {
        if (!_resultadoValidacion.EsValido)
        {
            throw new Exception("Antes de registrar se debe validar el pedido.");
        }

        var cliente = _repositorioCliente.BuscarPorCodigo(pedido.CodigoCliente);
        if(cliente == null)
        {
           throw new Exception("El cliente no existe.");
        }        
        //codigo
    }
    private IEnumerable<string> ObtenerReglasNoCumplidas(Pedido pedido)
    {
        if (pedido.CantidadProductos > 3)
        {
            yield return "La cantidad de elementos debe ser menor a 3";
        }

        if (pedido.MontoTotal > 1000)
        {
            yield return "El monto del pedido debe ser menor a 1000";
        }
    }
}

[HttpPost]
public IEnumerable<string> RegistrarPedido(Pedido pedido)
{
    var servicioPedido = new ServicioPedidoOferta();
    var resultadoValidacion = servicioPedido.Validar(pedido);
    if (resultadoValidacion.EsValido())
    {
        servicioPedido.Registrar(pedido);
    }
    else
    {
        return resultadoValidacion.Mensajes;
    }
}
Conclusión:

No se deben usar las excepciones para manejar el flujo de una aplicación, recuerden que una excepción es un comportamiento no esperado que no permite que se ejecute correctamente una función. Los errores son muy costoso y hay que evitarlos sino el rendimiento de nuestro aplicativo se verá bajando en el tiempo. Lo recomendado es que primero se validen los parámetros de entrada o las reglas previas antes de llamar a un método, pero se debe tener claro que cada función debe protegerse de los parámetros que recibe para que garantice su correcto funcionamiento.

Referencias:
Metal Tip:

Este artículo lo escribí escuchando la canción Necrophiliac de la banda Slayer de Estados Unidos, les comparto el enlace.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s