Result Pattern: Melhorando a legibilidade da sua aplicação

Francisco Junior
Design Patterns Arquitetura

Na implementação de qualquer caso de uso, há um “caminho feliz” onde tudo ocorre conforme o esperado: desde a entrada das informações, a conexão com o banco de dados, até as integrações com outros sistemas, resultando na conclusão bem-sucedida do caso de uso. Por exemplo, em um método simples cujo objetivo é converter uma string em um número inteiro, podemos ter o seguinte código:

public int ConvertToInt(string value)
{
		return Int32.Parse(value);
}

Aqui estamos considerando que o valor recebido será sempre válido e não causará erro. No entanto, também existem fluxos alternativos. Caso não seja possível converter o valor para inteiro, a aplicação deve prever essa situação e tratá-la da melhor forma.

public int ConvertToInt(string value)
{
    try
    {
        return Int32.Parse(value);
    }
    catch
    {
        throw new Exception("Value is invalid");
    }
}

Ou até mesmo dessa forma:

public int ConvertToInt(string value)
{
    if (Int32.TryParse(value, out int result))
    {
        return result;
    }

    throw new Exception("Value is invalid");
}

Embora pareça muito conveniente utilizar exceções para controle de fluxo, inclusive no .Net 8 com o manipulador de exceções global ao invés da utilização de um middleware, isso não é controle de fluxo. O manipulador de exceções global geralmente encerrará a execução porque indica que ocorreu um erro irrecuperável. Controle de fluxo significa que você tem a oportunidade de investigar o erro e decidir como proceder de uma maneira que permita ao código continuar “fluindo” naturalmente. Exceções, por definição, interrompem o fluxo e, portanto, não podem ser usadas como controle de fluxo.

Result Pattern

O uso do Result Pattern oferece várias vantagens significativas sobre o uso de exceções para o controle de fluxo, especialmente no que diz respeito a legibilidade, desempenho e manutenção do código. 

Legibilidade do código: O Result Pattern deixa claro que a função pode falhar e quais tipos de falha podem ocorrer, sem a necessidade de ler uma lista de exceções possíveis. Isso torna o código mais fácil de entender.

Desempenho: Evita o custo de construir e manipular pilhas de exceções, resultando em melhor desempenho, especialmente em cenários onde falhas são esperadas como parte do fluxo normal do programa.

Manutenabilidade: Melhora a capacidade de manutenção do código ao tornar o fluxo de erro explícito e reduzindo a necessidade de documentar e rastrear exceções que podem ser lançadas.

Tratamento inadequado de erros: Exceções devem ser reservadas para situações verdadeiramente excepcionais e inesperadas, não para o fluxo de controle normal. O Result Pattern promove essa prática, incentivando um design de software mais robusto e intuitivo.

Implementação

Inicialmente vamos criar nossa estrutura de erro. Ela possui apenas dois parâmetros mas podemos incrementar com mais informações dependendo da necessidade. Nossa struct implementa o método Failure como builder para criar nosso objeto de erro.

public readonly record struct Error
{
    private Error(string code, string description)
    {
        Code = code;
        Description = description;
    }
    
    public string Code { get; }
    
    public string Description { get; }
    
    public static Error Failure(
        string code = "General.Failure",
        string description = "A failure has occurred.") =>
        new(code, description);
};

Abaixo teremos nossa struct Resut, é um tipo genérico inde vai receber o resultado da nossa operação ou um ou mais erros que cairam em nosso fluxo de exceção.

public readonly record struct Result<TValue>
{
    private readonly TValue? _value = default;
    private readonly List<Error>? _errors = null;

    public Result()
    {
        throw new InvalidOperationException();
    }

    public Result(Error error)
    {
        _errors = [error];
    }
    
    public Result(List<Error> errors)
    {
        if (errors is null)
        {
            throw new ArgumentNullException(nameof(errors));
        }

        if (errors is null || errors.Count == 0)
        {
            throw new ArgumentException("Cannot create an Result<TValue> from an empty collection of errors.", nameof(errors));
        }

        _errors = errors;
    }

    public Result(TValue value)
    {
        if (value is null)
        {
            throw new ArgumentNullException(nameof(value));
        }

        _value = value;
    }
    
    public bool IsError => _errors is not null;
    
    public TValue Value
    {
        get
        {
            if (IsError)
            {
                throw new InvalidOperationException("Check IsError before accessing Value.");
            }

            return _value;
        }
    }
}

Agora podemos ver a implementação do nosso método que realiza a conversão de uma string para inteiro utilizando o Result Pattern.

public Result<int> ConvertToInt(string value)
{
    
    if (Int32.TryParse(value, out int result))
    {
        return new Result<int>(result);
    }

    return new Result<int>(Error.Failure());
}

Podemos melhorar essa solução tornando os construtores privados e fazendo o uso do implicit operator para tornar o código mais legível.

public static implicit operator Result<TValue>(TValue value)
{
    return new Result<TValue>(value);
}

public static implicit operator Result<TValue>(Error error)
{
    return new Result<TValue>(error);
}

public static implicit operator Result<TValue>(List<Error> errors)
{
    return new Result<TValue>(errors);
}

Logo, nossa implementação ficará dessa forma:

public Result<int> ConvertToInt(string value)
{
    
    if (Int32.TryParse(value, out int result))
    {
        return result;
    }

    return Error.Failure();
}

Conclusão

Diferente do Result Pattern, exceções são projetadas para lidar com condições inesperadas, não para controlar o fluxo da sua aplicação. Usá-las para essa finalidade distorce o propósito original das exceções e pode levar a um design de software menos intuitivo. O Result Pattern também não requer métodos ou abstrações complexas tornando sua implementação bastante simples.

Existem alguns pacotes NuGet que implementam o Result Pattern, como o FluentResults e o ErrorOr, que podem ser facilmente configurados e utilizados em seu projeto. Alternativamente, você pode implementar seu próprio Result Pattern.

Repositório: https://github.com/FcoJunior/ResultPattern

Compartilhe esse post