Uma outra abordagem para tratar falhas (e exceptions)

Por 24 outubro, 2016CSharp, Programação

Tempo de leitura: 2 minutos

O que você acha desta função?

public static double Divide(double a, double b)
{
    if (Math.Abs(b) < 0.0001)
        throw new DivideByZeroException();

    return a/b;
}

A princípio tudo certo. Não é? Entretanto, há algo que me incomoda bastante nesta implementação: estar lançando um exception.

Gosto do conceito de exceptions porque, assim, o uso indevido de uma função passa "em branco". Entretanto, não há garantias de que o programador "cliente" se preocupe em tratar exceptions e, desta forma, uma potencial instabilidade no sistema pode ser gerada (Sem dúvidas, um efeito colateral indesejado).

Uma abordagem superior

Há tempos venho defendendo a ideia de que nosso código deve refletir nossas intenções. Por isso, me agrada muito a ideia de melhorar a assinatura dos métodos deixando claro o que deve ser esperado.

Caso não haja garantias de que um método ou função possa gerar o resultado esperado, isso deve ficar explícito na assinatura. Isso pode ser obtido através do uso de um container.

public struct Try<TFailure, TSuccess>
{
    internal TFailure Failure { get; }
    internal TSuccess Success { get; }

    public bool IsFailure { get; }
    public bool IsSucess => !IsFailure;

    internal Try(TFailure failure)
    {
        IsFailure = true;
        Failure = failure;
        Success = default(TSuccess);
    }

    internal Try(TSuccess success)
    {
        IsFailure = false;
        Failure = default(TFailure);
        Success = success;
    }

    public static implicit operator Try<TFailure, TSuccess>(TFailure failure)
        => new Try<TFailure, TSuccess>(failure);

    public static implicit operator Try<TFailure, TSuccess>(TSuccess success)
        => new Try<TFailure, TSuccess>(success);

    public TResult Match<TResult>(Func<TFailure, TResult> failure, Func<TSuccess, TResult> success)
        => IsFailure ? failure(Failure) : success(Success);

    public Unit Match(Action<TFailure> failure, Action<TSuccess> success)
        => Match(ToFunc(failure), ToFunc(success));
}

public struct Unit
{}

public static partial class Helpers
{
    private static readonly Unit unit = new Unit();
    public static Unit Unit() => unit;

    public static Func<T, Unit> ToFunc<T>(Action<T> action) => o =>
    {
        action(o);
        return Unit();
    };
}

Phew! Agora, podemos melhorar nosso método!

public static Try<Exception, double> Divide(double a, double b)
{
    if (Math.Abs(b) < 0.0001) return new DivideByZeroException();
    return a/b;
}

Muito melhor! Agora a assinatura da função indica claramente que ela pode falhar. Além disso, pela natureza do container, temos garantias de que o "programador cliente" irá implementar a validação adequada.

public static void Foo()
{
    Divide(4, 0).Match(
        failure: e => WriteLine("divisor cannot be zero"),
        success: r => WriteLine($"result = {r}")
        );
}

Sem efeitos colaterais...

Próximos passos..

O uso de um container como o que construímos aqui abre espaço para melhoremos consideravelmente a legibilidade de códigos que implementem fluxos onde exista dependência do resultado da execução de uma operação para as seguintes ... Mas isso é assunto para outro post.

Deixe aqui seu comentário... 21 Comentários

  • É uma das coisas que invejo o Java, a possibilidade de fazer isso com suporte da própria linguagem.
    public void someMethod() throws someCheckedException {
    }

  • Luiz Adolphs disse:

    Muito massa! Parabéns pelo artigo!!!!

  • Essa abordagem lembra muito as discriminated unions e o pattern matching do F# (e outras linguagens), que são conceitos muito bacanas! 👍

  • Essa abordagem lembra muito as discriminated unions e o pattern matching do F# (e outras linguagens), que são conceitos interessantíssimos! 👍

  • Não é o mesmo que uma `checked exception` no Java? E a galera de lá já discutiu bastante que a ideia, na prática, não é tão boa assim!

  • Não é o mesmo que uma `checked exception` no Java? E a galera de lá já discutiu bastante que a ideia, na prática, não é tão boa assim. O que vc acha?

  • José Roberto Araújo disse:

    Excelente abordagem esse conceito de Containers, encapsulando algumas responsabilidades e, além disso, possibilitando a extensão do código, melhorando a legibilidade, segurança de execução do código, intenção na implementação do código. Parabéns mais uma vez por esse Post! Pensar assim é pensar out-of-the-box.

  • Bruno Custódio disse:

    A lógica para forçar a implementação achei bacana. Mas não gosto de usar exception para tratar regras de negócio da aplicação. Para tratar regras de negócio acho válido o Notification Pattern. Pois uma exception exige mais processamento do que uma mensagem de erro.

    Para quem quer saber mais, da uma olhada em http://martinfowler.com/eaaDev/Notification.html

  • Luciano disse:

    Você não gosta de exception por uma questão de performance ou pelo fato de algum código cliente não tratar?

    • elemarjr disse:

      Não gosto de exceptions pelo padrão não determinístico que ela implica no sistema. Não há problema em um método retornar uma falha.

  • Andre Camilo disse:

    é um conceito funcional.... ótimo

  • Fabricio Doi disse:

    Gostei como expressividade da função exposta, que me parece ser o maior ganho disso. Porém nada impede que o programador ignore o tratamento da exceção usando uma ação vazia, como já vi muito em Java com os "catches" que não fazem absolutamente nada, e é o principal problema apontado em 'checked exception'.
    Para mim, esse padrão é MELHOR que o 'checked exception' pois:
    1. Pode ser encarada como independente de linguagem, podendo ser usada até mesmo em JAVA.
    2. Como dito em comentários anteriores, não tem o peso do disparo de exceções.
    3. É opcional, não é imposto pela plataforma que está sendo usada.
    Por outro lado, não deixa claro os tipos de Exceção a que a função está exposta, o que não me parece difícil de fazer, porém pode aumentar a complexidade do código desnecessariamente.
    A entrevista citada com Anders Hejlsberg http://www.artima.com/intv/handcuffsP.html fala que o C# não implementou 'checked exception' pois o time não encontrou um jeito de fazê-lo sem os defeitos do JAVA e procurou seguir a premissa de simplicidade. A entrevista é de 2003, se em 13 anos não encontraram uma forma de resolver isso me parece que não estão tão preocupados com isso, ou que é um problema muito maior para ser estudado.

  • Fabricio Doi disse:

    Uma questão apenas, a implementação fica bonita, mas como analiso um erro que aconteceu em meu código? Com o lançamento da exceção (throw) chegam informações úteis em dev, como a linha em que ocorreu a exceção. Deste jeito, como poderia encontrar isso? O C# agora possui o nameof que auxiliaria nisso, mas seria suficiente?

  • Mario disse:

    Elemar, desculpa a ignorância, mas estou tentando implementar a struct Try e estou obtendo um erro na seguinte linha "public bool IsSucess => !IsFailure;" a IDE acusa erro na expressão "=>" informando que está faltando ";".
    Se puder me ajudar agradeço, estava procurando um solução desse tipo, para aumentar a confiabilidade das minhas aplicações.

Deixe uma Resposta