Refatoração · C#

Simplificando um resultado com herança, imutabilidade e construtores primários

Uma comparação entre duas formas de modelar SecondFactorResult — do uso de flags com string até hierarquia de tipos.

Publicado em 2026 6 min de leitura

Neste artigo, comparamos duas versões de um conjunto de classes que representam o resultado de um segundo fator de autenticação. O objetivo é mostrar como pequenas escolhas de modelagem — uso de flags, tipos concretos, imutabilidade e construtores primários — impactam diretamente na clareza e na segurança do código.


Antes

A modelagem original usava uma struct com constantes de string para representar o estado do resultado, além de uma classe base concreta com um campo SecondFactorResultCode mutável e um método estático auxiliar para o estado "desabilitado".

Código original
public struct SECOND_FACTOR_RESULT_CODE
{
    public const string CREATED = "CREATED";
    public const string VALIDATED = "VALIDATED";
    public const string DISABLED = "DISABLED";
}

public class SecondFactorResult
{
    public string SecondFactorResultCode { get; set; }

    public static SecondFactorResult SecondFactorDisabledToOperation() {
        return new SecondFactorResult() { SecondFactorResultCode = SECOND_FACTOR_RESULT_CODE.DISABLED };
    }
}

public class SecondFactorCreatedResult : SecondFactorResult
{
    public object AuthorizationRequestCode { get; set; }
    public SecondFactorCreatedResult(GenerateAuthorizationRequestCodeResponse authorizationRequestCode)
    {
        SecondFactorResultCode = SECOND_FACTOR_RESULT_CODE.CREATED;
        AuthorizationRequestCode = authorizationRequestCode;
    }
}

public class SecondFactorValidatedResult : SecondFactorResult
{
    public string SecondFactorMethodId { get; set; }
    public string AuthorizationRequestCodeId { get; set; }
    public string AuthorizationRequestBiometricSignatureId { get; set; }

    public SecondFactorValidatedResult()
    {
        SecondFactorResultCode = SECOND_FACTOR_RESULT_CODE.VALIDATED;
    }
}

Exemplo de utilização

Com essa modelagem, o consumidor precisava checar o campo SecondFactorResultCode para descobrir o que fazer e, em seguida, fazer cast para acessar os dados específicos de cada estado. O default era necessário para cobrir strings desconhecidas — algo que o compilador não impedia.

switch sobre a string do código
switch (result.SecondFactorResultCode)
{
    case SECOND_FACTOR_RESULT_CODE.DISABLED:
        Console.WriteLine("Segundo fator desabilitado");
        break;

    case SECOND_FACTOR_RESULT_CODE.CREATED:
        var createdResult = (SecondFactorCreatedResult)result; // cast necessário
        Console.WriteLine($"Código enviado: {createdResult.AuthorizationRequestCode}");
        break;

    case SECOND_FACTOR_RESULT_CODE.VALIDATED:
        var validatedResult = (SecondFactorValidatedResult)result; // cast necessário
        Console.WriteLine($"Método: {validatedResult.SecondFactorMethodId}");
        break;

    default:
        throw new InvalidOperationException($"Código desconhecido: {result.SecondFactorResultCode}");
}

Depois

A versão refatorada transforma a classe base em abstract, elimina a struct de constantes e passa a representar cada estado como um tipo concreto. As propriedades ficam imutáveis e a tipagem fica mais forte. Construtores primários do C# 12 também são usados para reduzir boilerplate.

Código refatorado
public abstract class SecondFactorResult
{ }

public class SecondFactorDisabledResult : SecondFactorResult
{ }

public class SecondFactorCreatedResult(GenerateAuthorizationRequestCodeResponse AuthorizationRequestCode) : SecondFactorResult
{
    public GenerateAuthorizationRequestCodeResponse AuthorizationRequestCode { get; } = AuthorizationRequestCode;
}

public class SecondFactorValidatedResult(string SecondFactorMethodId, string AuthorizationRequestCodeId = null, string AuthorizationRequestBiometricSignatureId = null) : SecondFactorResult
{
    public string SecondFactorMethodId { get; } = SecondFactorMethodId;
    public string AuthorizationRequestCodeId { get; } = AuthorizationRequestCodeId;
    public string AuthorizationRequestBiometricSignatureId { get; } = AuthorizationRequestBiometricSignatureId;
}

Exemplo de utilização

Com o modelo novo, o switch faz pattern matching sobre o tipo do resultado. Não há mais cast, nem consulta a strings, e o acesso às propriedades específicas é direto no case.

switch com pattern matching sobre o tipo
switch (result)
{
    case SecondFactorDisabledResult:
        Console.WriteLine("Segundo fator desabilitado");
        break;

    case SecondFactorCreatedResult createdResult:
        Console.WriteLine($"Código enviado: {createdResult.AuthorizationRequestCode}");
        break;

    case SecondFactorValidatedResult validatedResult:
        Console.WriteLine($"Método: {validatedResult.SecondFactorMethodId}");
        break;
}
Sem casts, sem default com exceção e sem risco de o resultado carregar uma string inválida. O compilador garante que os case só acessam propriedades que existem naquele tipo.

O que mudou

Herança no lugar de flags

A classe base deixou de carregar uma string de código para indicar o tipo de resultado. Cada estado agora é uma subclasse: SecondFactorDisabledResult, SecondFactorCreatedResult e SecondFactorValidatedResult. O próprio tipo passa a ser o discriminador, o que permite pattern matching e torna o código mais seguro.

Fim da struct de constantes

SECOND_FACTOR_RESULT_CODE não existe mais. Antes, era possível atribuir qualquer string ao campo SecondFactorResultCode, inclusive uma que não representasse um estado válido. Com subclasses, essa possibilidade desaparece.

Imutabilidade

Todas as propriedades agora têm apenas get, com atribuição feita no construtor ou no inicializador. Uma instância de resultado não pode mais mudar de estado depois de criada — algo importante quando o objeto é passado adiante na aplicação.

Tipagem forte

AuthorizationRequestCode era object. Agora é GenerateAuthorizationRequestCodeResponse, eliminando casts e deixando o contrato explícito.

Construtores primários

SecondFactorCreatedResult e SecondFactorValidatedResult usam construtores primários. Os parâmetros alimentam diretamente as propriedades, sem repetir declarações e atribuições no corpo do construtor.

Fim do método estático auxiliar

SecondFactorDisabledToOperation() existia apenas para criar uma instância com código DISABLED. Agora basta instanciar new SecondFactorDisabledResult() — o nome do tipo já comunica o estado.

Parâmetros opcionais no validado

SecondFactorValidatedResult recebe SecondFactorMethodId como obrigatório e os demais campos como opcionais (= null), refletindo com mais precisão quais dados são realmente necessários para cada caso.

O resultado é um modelo mais alinhado com os princípios de orientação a objetos: polimorfismo real, imutabilidade, tipagem forte e ausência de constantes mágicas. O uso de switch com pattern matching sobre o tipo do resultado também se torna natural.