Engenharia de Software5 min de leitura

Result Pattern: controle de fluxo da forma correta

Como usar o Result Pattern para tornar falhas esperadas explícitas, evitar exceções como controle de fluxo e melhorar a legibilidade em aplicações .NET.

Result Pattern: controle de fluxo da forma correta

Engenharia de Software

dotnet, arquitetura, result-pattern, boas-praticas

pt

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 conveniente utilizar exceções para controle de fluxo, inclusive no .NET 8 com o manipulador de exceções global em vez 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 deveriam 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 controle de fluxo, especialmente no que diz respeito à 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 reduzir 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 incrementá-la 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 Result. Ela é um tipo genérico onde vamos receber o resultado da nossa operação ou um ou mais erros que caíram 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("The Value property cannot be accessed when errors have been recorded. 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 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. Este padrão é cross-cutting, ou seja, pode ser aplicado desde as camadas mais internas até as camadas mais externas da sua aplicação.

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

#dotnet#arquitetura#result-pattern#boas-praticas
Francisco Junior

Olá, eu sou Francisco Junior.

Sou Software Engineer e Tech Lead.

Trabalho diariamente desenvolvendo plataformas financeiras distribuídas. Neste blog compartilho aprendizados sobre arquitetura, engenharia, cloud, plataforma, liderança técnica e decisões reais encontradas durante o desenvolvimento de software.

Acompanhe também