24 outubro, 2016 21 Comentários AUTOR: elemarjr CATEGORIAS: CSharp, Programação Tags:,

Uma outra abordagem para tratar falhas (e exceptions)

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.

21 Comentários

  1. Alberto Monteiro 4 meses ago says:

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

    Responder
    • elemarjr 4 meses ago says:

      Também gosto disso em Java. Mas, parece que o pessoal de Java não gosta.

      Responder
      • Alberto Monteiro 4 meses ago says:

        Grande Elemar Rodrigues Severo Junior tomei a liberdade de fazer uma pequena alteração no código para possibilitar encadeamento de Matchs quando lancem exceção, podendo fazer isso:
        Divide(4, 1)
        .Match(failure: e => WriteLine(e.Message),
        success: value =>
        {
        if (value == 4)
        return new Exception("Hi");
        return 1.AsTry();
        })
        .Match(failure: e => WriteLine(e.Message),
        success: value => WriteLine(value));
        Nesse link tem o gist completo: https://gist.github.com/AlbertoMonteiro/6aa46940d431ef8ff187723ac734853a

        Responder
      • Luciano 4 meses ago says:

        Existe uma entrevista de um dos idealizadores do c# que explica pq não esse recurso do Java entrou no C#. Foi uma decisão de projeto. Eu já gostei desse recurso no Java, hoje não gosto.

        Responder
        • elemarjr 4 meses ago says:

          Você deve estar falando das "checked exceptions". Repare que não é disso que eu trato aqui.

          Responder
  2. Luiz Adolphs 4 meses ago says:

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

    Responder
  3. Rafael Escobar 4 meses ago says:

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

    Responder
  4. Rafael Escobar 4 meses ago says:

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

    Responder
  5. Maurício Aniche 4 meses ago says:

    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!

    Responder
  6. Maurício Aniche 4 meses ago says:

    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?

    Responder
  7. José Roberto Araújo 4 meses ago says:

    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.

    Responder
  8. Bruno Custódio 4 meses ago says:

    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

    Responder
    • elemarjr 4 meses ago says:

      Repare que, na minha implementação, exceptions não são disparadas (o que a tornaria pesada).

      Responder
      • Leandro 4 meses ago says:

        Verdade, o uso do tipo exception em sí não pesa nada.

        Responder
  9. Luciano 4 meses ago says:

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

    Responder
    • elemarjr 4 meses ago says:

      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.

      Responder
  10. Andre Camilo 4 meses ago says:

    é um conceito funcional.... ótimo

    Responder
  11. Fabricio Doi 3 meses ago says:

    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.

    Responder
  12. Fabricio Doi 3 meses ago says:

    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?

    Responder
  13. Mario 2 dias ago says:

    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.

    Responder
    • elemarjr 2 dias ago says:

      Veja a implementação que está disponível no meu GitHub.

      Responder