C# - Substituindo bloco switch/case, if/else por polimorfismo


Hoje vamos mostrar porque e quando substituir o bloco de código condicional por polimorfismo na linguagem C#,

Na programação orientada a objetos você vai encontrar com abundância a utilização do if/else e o switch/case que pode realizar diversas ações dependendo do tipo de objeto ou propriedade.

Mas é errado usar if/else ou switch/case no código ?

Claro que não.

O problema ocorre quando temos que fazer a verificação de um tipo de objeto a fim de tomar alguma decisão lógica usando blocos if/else ou switch/case.

Nestes casos quando você está fazendo verificações por tipo e executando algum tipo de operação, é uma boa ideia encapsular esse algoritmo dentro da classe e, em seguida, usar o polimorfismo para abstrair a chamada ao código.

Vejamos um exemplo prático que mostra como podemos fazer isso usando um exemplo simples em um projeto Console com o .NET 5.0.

Neste exemplo vamos usar a instrução switch/case.

Vamos supor que temos uma classe Vendedor e uma Enumeração definindo 3 níveis de experiência: Junior, Master e Senior:

    public enum Experiencia
    {
        Junior = 1,
        Master = 2,
        Senior = 3
    }

    public class Vendedor
    {
        public string Nome { get; }
        public int Vendas { get; }
        public Experiencia Experiencia { get; }
        public Vendedor(string nome, int vendas, Experiencia experiencia)
        {
            Nome = nome;
            Vendas = vendas;
            Experiencia = experiencia;
        }
   }

Agora suponha que com base na experiência do vendedor precisamos calcular as metas de vendas, e para isso criamos a classe CalculaMetaVendas:

    public class CalculaMetaVendas
    {
        public static int CalculaVendas(Vendedor vendedor)
        {
            switch (vendedor.Experiencia)
            {
                case Experiencia.Junior:
                    return 5;
                case Experiencia.Master:
                    return 10;
                case Experiencia.Senior:
                    return 20;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }

Estou simplificando a lógica do cálculo das metas de vendas mas em geral aqui teríamos uma lógica mais complexa.

Agora podemos usar os artefatos criados e verificar o seu funcionamento:

    class Program
    {
        static void Main(string[] args)
        {
            var vendedor = new Vendedor("Macoratti", vendas: 5, experiencia: Experiencia.Junior);
            var metaVendas = CalculaMetaVendas.CalculaVendas(vendedor);
            Console.WriteLine($"Vendedor = {vendedor.Nome} Vendas = {vendedor.Vendas} Experiência ={vendedor.Experiencia}");
            Console.WriteLine($"Meta de Vendas = {metaVendas}");
        }
    }

Esta tudo funcionando mas temos um problema com o bloco switch/case usado para calcular as metas de vendas.

Se houver a introdução de um novo requisito para o cálculo de vendas vamos ter que alterar a instrução switch e o código vai aumentar e teremos mais código procedural sendo adicionado.

Vamos supor que no nosso caso o cálculo das metas de vendas foi alterado para levar em conta também as vendas do vendedor.

    public class CalculaMetaVendas
    {
        public static int CalculaVendas(Vendedor vendedor)
        {
            switch (vendedor.Experiencia)
            {
                case Experiencia.Junior:
                    if (vendedor.Vendas < 10)
                        return 10;
                    else
                        return 20;
                case Experiencia.Master:
                    if (vendedor.Vendas < 20)
                        return 20;
                    else
                        return 30;
                case Experiencia.Senior:
                    if (vendedor.Vendas < 30)
                        return 30;
                    else
                        return 40;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }    

O código ficou mais complexo e mais difícil de entender, de manter e de testar.

No entanto podemos melhorar o código substituindo as instruções switch/case condicionais usando o polimorfismo. Vejamos como fazer isso.

Substituindo switch/case condicional por polimorfismo

Vamos usar o recurso do polimorfismo e da herança, dois pilares do paradigma da programação orientada a objetos para tornar nosso código mais robusto.

Vamos iniciar refatorando a classe Vendedor:

    public abstract class Vendedor
    {
        public string Name { get; }
        public int Vendas { get; }
        protected Vendedor(string nome, int vendas)
        {
            Name = name;
            Vendas = vendas;
        }
    }

Nossa classe Vendedor agora é uma classe abstrata e vai servir como uma classe base não podendo mais ser instanciada.

A seguir vamos refatorar o código da classe CalculaMetaVendas para remover o switch/case.

Vamos criar a interface ICalculaMetaVendas onde vamos definir o método Calcular() que vai calcular a meta de vendas com base no vendedor.

    public interface ICalculaMetaVendas
    {
        int Calcular(Vendedor vendedor);
    }

Vamos agora criar 3 classes, uma para cada tipo de vendedor:

  1. VendedorJunior
  2. VendedorMaster
  3. VendedorSenior

Essas classes devem herdar da classe base Vendedor e implementar o cálculo da meta de vendas usando a interface ICalculaMetaVendas  que definimos com base no tipo de vendedor. Este método vai para o método calcular do tipo da interface a instância do vendedor atual:

 1- VendedorJunior

    public class VendedorJunior : Vendedor
    {
        public VendedorJunior(string nome, int vendas) : base(nome, vendas) { }
        public int CalculaMetaVendas(ICalculaMetaVendas calcularMeta)
            => calcularMeta.Calcular(this);
    }

A seguir vamos criar a classe VendedorJuniorCalculaMeta que calcula efetivamente a meta para o tipo de vendedor implementando a interface ICalculaMetaVendas :

    public class VendedorJuniorCalculaMeta : ICalculaMetaVendas
    {
        public int Calcular(Vendedor vendedor)
            => vendedor.Vendas < 10 ? 10 : 20;
    }

Agora basta repetir o procedimento para VendedorMaster e VendedorSenior. (fica como exercício...)

Vamos testar a nossa implementação:

     class Program
    {
        static void Main(string[] args)
        {
            var vendedor = new VendedorJunior("Macoratti", vendas: 5);
            var meta = new VendedorJuniorCalculaMeta();
            var metaVendas = vendedor.CalculaMetaVendas(meta);
            Console.WriteLine($"Vendedor = {vendedor.Nome} Vendas = {vendedor.Vendas}");
            Console.WriteLine($"Meta de Vendas = {metaVendas}");
            Console.ReadKey();
        }
    }

Assim temos a solução que substituiu a utilização do bloco swtich/case usando polimorfismo.

Com esta solução, agora estamos livres para criar novas classes que implementam a interface ICalculaMetaVendas . Quando novos requisitos foram necessários, podemos escrever novas classes focadas, em vez de escrever mais lógica no bloco switch. Assim estaremos aderente ao princípio OCP (Aberto/Fechado).

Observe que a enumeração também foi substituída por objetos. Agora podemos apresentar classes como VendedoJunior, VendedorMaster e VendedorSenior , e,  temos assim comportamento e dados agora reunidos em uma classe coesa.

Pegue o projeto aqui: CShp_UsandoPolimorfismo.zip

"17 Quando o vi (Jesus), caí aos seus pés como morto. Então ele colocou sua mão direita sobre mim e disse: "Não tenha medo. Eu sou o primeiro e o último. 18 Sou aquele que vive. Estive morto mas agora estou vivo para todo o sempre! E tenho as chaves da morte e do Hades."
Apocalipse 1:17,18

Referências:


José Carlos Macoratti