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.
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".
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 (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.
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 (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; }
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.
switch com pattern matching sobre o tipo do resultado também se torna natural.