Executando verificação de segurança...
1

🔢 Enums Fortemente Tipados em C# - Um Guia Completo 💪

Enums em C# são amplamente usados para representar um conjunto de valores constantes, mas eles possuem limitações, como a impossibilidade de anexar comportamentos ou dados adicionais diretamente a esses valores. Neste artigo, vamos abordar essas limitações e, principalmente, criar uma implementação de enums fortemente tipados que adiciona comportamento e dados aos valores dos enums.

Limitações dos Enums em C#

Vamos iniciar com uma implementação simples de um enum de cartão de crédito, onde temos tipos de cartão (Padrão, Premium, Platinum) e um switch que retorna um desconto baseado no tipo do cartão. Abaixo, vemos uma estrutura básica de código que imprime o tipo de cartão e o desconto:

enum CreditCard
{
    Standard,
    Premium,
    Platinum
}

// Uso de switch para definir desconto baseado no tipo de cartão
var cardType = CreditCard.Platinum;
double discount = cardType switch
{
    CreditCard.Standard => 0.01,
    CreditCard.Premium => 0.05,
    CreditCard.Platinum => 0.1,
    _ => 0
};

Console.WriteLine($"O desconto para {cardType} é {discount * 100}%");

Essa abordagem funciona, mas traz alguns problemas:

  1. Manutenção: Ao adicionar um novo valor ao enum, é necessário atualizar o switch.
  2. Extensibilidade: Enums convencionais não permitem que comportamentos e dados específicos sejam atribuídos diretamente aos valores.

Para resolver isso, vamos implementar um enum fortemente tipado.

Construindo um Enum Fortemente Tipado

Para começar, criaremos uma classe abstrata Enumeration que servirá como base para nossos enums fortemente tipados. Ela terá duas propriedades, Value e Name, que serão únicas para cada valor do enum. Esse padrão permite que adicionemos comportamentos específicos e simplifica a manipulação dos valores.

Estrutura da Classe Enumeration

A classe Enumeration é genérica e implementa a interface IEquatable, que ajuda na comparação de instâncias. Abaixo está a estrutura inicial:

public abstract class Enumeration<TEnum> : IEquatable<Enumeration<TEnum>>
    where TEnum : Enumeration<TEnum>
{
    public int Value { get; }
    public string Name { get; }

    protected Enumeration(int value, string name)
    {
        Value = value;
        Name = name;
    }

    public bool Equals(Enumeration<TEnum>? other)
    {
        if (other == null) return false;
        return GetType().Equals(other.GetType()) && Value.Equals(other.Value);
    }

    public override bool Equals(object? obj)
    {
        return obj is Enumeration<TEnum> other && Equals(other);
    }

    public override int GetHashCode() => Value.GetHashCode();
}

Aqui, Value e Name são inicializados apenas uma vez, garantindo que cada valor de enum tenha uma representação única. Também implementamos os métodos Equals e GetHashCode para permitir comparações.

Métodos Estáticos para Recuperação de Valores

Adicionaremos métodos para buscar instâncias específicas do enum com base no Value ou Name. Usaremos um dicionário para armazenar e acessar rapidamente os valores, e métodos CreateEnumerationsByValue e CreateEnumerationsByName para inicializar esses dicionários:

private static readonly Dictionary<int, TEnum> EnumerationsByValue = CreateEnumerationsByValue();
private static readonly Dictionary<string, TEnum> EnumerationsByName = CreateEnumerationsByName();

public static TEnum? FromValue(int value)
{
    return EnumerationsByValue.TryGetValue(value, out var enumValue) ? enumValue : null;
}

public static TEnum? FromName(string name)
{
    return EnumerationsByName.TryGetValue(name, out var enumValue) ? enumValue : null;
}

private static Dictionary<int, TEnum> CreateEnumerationsByValue()
{
    return typeof(TEnum)
        .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.DeclaredOnly)
        .Where(f => f.FieldType == typeof(TEnum))
        .Select(f => (TEnum)f.GetValue(null)!)
        .ToDictionary(e => e.Value);
}

private static Dictionary<string, TEnum> CreateEnumerationsByName()
{
    return typeof(TEnum)
        .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.DeclaredOnly)
        .Where(f => f.FieldType == typeof(TEnum))
        .Select(f => (TEnum)f.GetValue(null)!)
        .ToDictionary(e => e.Name);
}

Esses métodos usam reflexão para identificar os campos públicos e estáticos que correspondem a valores do enum, construindo os dicionários EnumerationsByValue e EnumerationsByName.

Exemplo de Uso com Cartão de Crédito

Para ilustrar a funcionalidade, vamos aplicar nossa classe Enumeration criando uma classe CreditCard. Nessa classe, definimos cartões específicos e seus respectivos descontos.

Diagram do Exemplo de Uso com Cartão de Crédito

public abstract class CreditCard : Enumeration<CreditCard>
{
    public static readonly CreditCard Standard = new StandardCard();
    public static readonly CreditCard Premium = new PremiumCard();
    public static readonly CreditCard Platinum = new PlatinumCard();

    public abstract double Discount { get; }

    private CreditCard(int value, string name) : base(value, name) { }

    private sealed class StandardCard : CreditCard
    {
        public StandardCard() : base(1, "Standard") { }
        public override double Discount => 0.01;
    }

    private sealed class PremiumCard : CreditCard
    {
        public PremiumCard() : base(2, "Premium") { }
        public override double Discount => 0.05;
    }

    private sealed class PlatinumCard : CreditCard
    {
        public PlatinumCard() : base(3, "Platinum") { }
        public override double Discount => 0.1;
    }
}

Cada classe interna representa um tipo específico de cartão com um desconto específico. Esse design nos permite estender facilmente a classe para adicionar mais tipos de cartões com novos valores e comportamentos.

Implementação de Métodos Auxiliares e Uso

Para buscar um valor com base em dados externos (por exemplo, valor vindo de uma API), usamos o método FromValue ou FromName.

// Uso de FromValue para buscar um cartão pelo valor
var card = CreditCard.FromValue(1);
Console.WriteLine($"O desconto para {card?.Name} é {card?.Discount * 100}%");

// Uso de FromName para buscar um cartão pelo nome
var platinumCard = CreditCard.FromName("Platinum");
Console.WriteLine($"O desconto para {platinumCard?.Name} é {platinumCard?.Discount * 100}%");

Essa flexibilidade facilita a manipulação dos valores de enum sem precisar de switch ou verificações manuais adicionais.

Conclusão

A implementação de enums fortemente tipados em C# permite que trabalhemos com tipos mais seguros e estendíveis. Ao encapsular comportamento específico em cada valor do enum, reduzimos a complexidade do código, melhoramos a legibilidade e aumentamos a segurança, especialmente em cenários que exigem manutenção frequente ou personalização de valores. Essa abordagem é útil para sistemas que necessitam de controle adicional e personalizações específicas nos valores de enum, como configurações de desconto para diferentes tipos de cartões.

Esse é apenas um exemplo de como criar enums fortemente tipados. A mesma abordagem pode ser estendida para adicionar métodos abstratos adicionais que sejam implementados por cada instância, proporcionando ainda mais flexibilidade e controle.

Carregando publicação patrocinada...
Conteúdo excluído
1

A implementação de Enums Fortemente Tipados não é apenas uma questão de funcionalidade básica, como criar um Dictionary. A escolha pelo padrão que apresentei no artigo tem motivos claros que vão além de uma simples estrutura chave-valor. Vamos analisar e defender os principais pontos:


1. Semântica e Intenção

Um Dictionary é uma estrutura genérica de chave-valor, enquanto um Enum Fortemente Tipado fornece semântica clara. No exemplo do artigo, você não apenas define chaves e valores, mas encapsula comportamento e identidade, promovendo o uso de objetos ricos.

  • No caso do Dictionary:

    CreditCardDiscount["Standard"]
    

    Aqui, não há segurança de tipo. Se alguém digitar uma chave incorreta (e.g., "Stndard"), haverá um erro de runtime.

  • No caso do Enum Fortemente Tipado:

    CreditCardType.Standard.Discount
    

    Aqui, tanto a chave quanto o valor são validados em tempo de compilação, o que reduz drasticamente erros e torna o código mais expressivo.


2. Manutenção e Extensibilidade

Um Dictionary funciona bem para mapeamentos simples, mas torna-se limitado quando o modelo evolui. Por exemplo, se o CreditCard passar a ter outras propriedades além do desconto, como uma descrição ou prioridade:

  • No Dictionary:
    A abordagem ficaria mais complexa e menos legível, exigindo estruturas auxiliares ou novos mapeamentos:

    var CreditCardDetails = new Dictionary<string, (double Discount, string Description)>{
        { "Standard", (0.01, "Basic card") },
        { "Premium", (0.05, "Intermediate card") },
        { "Platinum", (0.1, "Top-tier card") }
    };
    
  • No Enum Fortemente Tipado:
    Adicionar comportamentos ou propriedades ao tipo é direto:

    public class CreditCardType
    {
        public static readonly CreditCardType Standard = new("Standard", 0.01, "Basic card");
        public static readonly CreditCardType Premium = new("Premium", 0.05, "Intermediate card");
        public static readonly CreditCardType Platinum = new("Platinum", 0.1, "Top-tier card");
    
        public string Name { get; }
        public double Discount { get; }
        public string Description { get; }
    
        private CreditCardType(string name, double discount, string description)
        {
            Name = name;
            Discount = discount;
            Description = description;
        }
    }
    

    Isso mantém o código coeso e organizado.


3. Segurança de Tipo

Ao utilizar um Dictionary, você depende de strings para as chaves, o que é suscetível a erros. Com Enums Fortemente Tipados, o tipo é explícito, o que impede uso incorreto ou ambíguo:

  • Com Dictionary:

    var discount = CreditCardDiscount["Stanndard"]; // Erro de runtime
    
  • Com Enum Fortemente Tipado:

    var discount = CreditCardType.Standard.Discount; // Seguro e validado em compile-time
    

4. Carga e Desempenho

Você mencionou no seu comentário "uma carga enorme". No entanto, a estrutura apresentada no artigo cria objetos imutáveis e reutilizáveis. Não há impacto significativo de desempenho, pois o modelo usa o padrão Singleton e evita reprocessamento.

Já o Dictionary carrega overheads adicionais de hashing e buscas dinâmicas, o que pode ser evitado na abordagem proposta.


5. Legibilidade e Organização

A proposta de um Dictionary resulta em menos legibilidade quando se trata de conceitos de domínio. A abordagem apresentada no artigo organiza os dados de forma mais explícita e mantém o contexto do domínio em uma única classe.

  • Um Enum Fortemente Tipado é autocontido e reflete melhor as intenções do código.

Ambas as abordagens têm méritos, mas a escolha por Enums Fortemente Tipados vai além de mapear valores: trata-se de melhorar a expressividade, a segurança de tipo, e a extensibilidade do sistema. Em um sistema que evolui, o custo inicial de implementação é compensado por um código mais robusto, organizado e seguro.