C# - Identificando instruções if-else como um problema
 Hoje vamos recordar como podemos verificar se as instruções if-else em nosso código estão se tornando um problema e vamos sugerir uma solução quando isso ocorre.


As instruções if-else podem causar muitos problemas se não forem usadas corretamente, podendo tornar o código difícil de ler, entender e manter e para piorar o cenários, as instruções if-else também podem levar a erros difíceis de identificar.

 


 


 

Isso geralmente ocorre quando se inclui outro recurso que não se encaixa na condição if usada. Ninguém quer um aplicativo que não seja estável ou que pare de funcionar de forma inesperada.

 

O Problema : Apresentando o cenário

 

Para ilustrar como isso pode ocorrer e como podemos resolver o problema vamos usar um cenário hipotético onde temos uma aplicação que gerencia pedidos e que sempre que um pedido é feito aciona uma lógica para realizar a entrega do pedido usando um meio de transporte.

 

Classe Pedido:

 

public class Pedido
{
  
public int Distancia { get; set; }
  
public int Peso { get; set; }
  
public bool Urgente { get; set; }

  
public override string ToString()
   {
    
return $"Distância: {Distancia}km, Peso: {Peso}kg, Urgente: {(Urgente ? "S" : "N")}";
   }
}


Criação do pedido :

 

void CriarPedido(Pedido pedido)
{
   
//Salvando o pedido

    //Lógistica de entrega do pedido
     TransportePedido(pedido);

}

   

Acima temos a classe Pedido e um trecho de código com um esboço que define a criação do pedido para o cenário descrito.

 

Como era de se esperar, depois de um tempo, seu aplicativo se tornou popular e passou a receber dezenas de pedidos de outros países, e, sua empresa fez um acordo com uma empresa de transporte aéreo para agilizar a entrega dos pedidos quando a distância for superior a 1000 Km.

 

Assim temos que implementar no código essa nova regra de negócio, e , isso foi feito da seguinte forma:

 

void CriarPedido(Pedido pedido)
{
  
//Salvando o pedido

   //Lógistica de entrega do pedido
  
if (pedido.Distancia < 1000)
   {
      TransporteRodoviario(pedido);
   }
  
else
   {
      TransporteAereo(pedido);
   }
}


Após algum tempo percebeu-se que o transporte aéreo dos pedidos fica muito caro quando o peso do pedido for superior a 100 Kg e que neste caso enviar o pedido via transporte marítimo seria mais viável. Entretanto caso o pedido seja urgente ele vai continuar sendo entregue via transporte aéreo mesmo pesando mais de 100 Kg.

 

Vamos então implementar esta nova sistemática no esboço do código descrito a seguir:

 

void CriarPedido(Pedido pedido)
{
  
//Salvando o pedido

   //Lógistica de entrega do pedido
  
if (pedido.Distancia < 1000 && !pedido.Urgente)
   {
     TransporteRodovidario(pedido);
   }
  
else if (pedido.Urgente || (pedido.Distancia >= 1000 && pedido.Peso <= 100))
   {
     TransporteAereo(pedido);
   }
  
else
   {
      TransporteMaritmo(pedido);
   }
}

Poderíamos continuar incluindo novos requisitos que com certeza iriam despontar com o passar do tempo o que nos levaria a ter que alterar novamente a implementação feita no  código.

Entretanto creio que você já percebeu que usar esta abordagem não é produtiva pois tivemos que modificar as condições existentes anteriormente no código que serem os princípios SOLID, prinicpalmente o princípio Open-Closed ou Aberto-Fechado.

Se continuarmos alterando o código incluindo novas regras relacionadas com a logística de entrega do pedido como entrega ferroviária, por exemplo, vamos acabar com um código cheio de instruções if-else contendo um lógica difícil de ler, entender e manter e também difícil de realizar testes de unidade.

Felizmente temos a solução para este problema :  respeitar os princípios SOLID.

A solução

A solução indica é usar os princípios SOLID como responsabilidade única e aplicar o princípio aberto-fechado.

 

Na implementação vamos criar um projeto Console no NET 6.

 

Vamos continuar usando a classe Pedido que não sofrerá nenhuma alteração :

 

public class Pedido
{
  
public int Distancia { get; set; }
  
public int Peso { get; set; }
  
public bool Urgente { get; set; }

  
public override string ToString()
   {
    
return $"Distância: {Distancia}km, Peso: {Peso}kg, Urgente: {(Urgente ? "S" : "N")}";
   }
}


A seguir vamos criar uma classe abstrata (pode ser uma interface) com método abstratos que definem a lógica de transporte. 

 

Vamos criar a classe Transportador contendo o método abstrato Transporte que aceita um Pedido como parâmetro :

 

public abstract class Transportador
{
  
public abstract void Transporte(Pedido pedido);
  
public void Dispose()
   {
     
// Suppress finalization.
      GC.SuppressFinalize(
this);
    }
}

A seguir vamos implementar a lógica de negócios relacionadas com os tipos de transporte : rodoviário, aéreo e marítimo e que estão relacionados com as respectivas classes.

Para isso vamos criar as classes TransporteRodoviario, TransporteAereo e TransporteMaritimo que vão herdar da classe abstrata Transportador e assim estamos aplicando o princípio da responsabilidade única onde cada classe tem apenas uma responsabilidade :

public class TransporteRodoviario : Transportador
{
  
public override void Transporte(Pedido pedido)
   {
     Console.WriteLine(
$"Pedido enviado via transporte rodoviário...");
    }
}
public class TransporteAereo : Transportador
{
  
public override void Transporte(Pedido pedido)
   {
     Console.WriteLine(
$"Pedido enviado via transporte aéreo...");     
    }
}
public class TransporteMaritimo : Transportador
{
  
public override void Transporte(Pedido pedido)
   {
     Console.WriteLine(
$"Pedido enviado via transporte maritimo...");  
    }
}

Aqui temos três classes separadas derivadas do Transportador e a mágica começa exatamente neste ponto.

Vamos adicionar um método abstrato IsAdequadoParaPedido que retorna um valor booleano e implementa condições para essas três classes.

Vamos primeiro definir o método na classe Transportador :

public abstract class Transportador
{
  
public abstract void Transporte(Pedido pedido);

  
protected abstract bool IsAdequadoParaPedido(Pedido pedido);


  
public void Dispose()
   {
     
// Suppress finalization.
      GC.SuppressFinalize(
this);
    }
}

A seguir vamos alterar as classes concretas que herda desta classe implementando este método e definindo a condição para a logística de entrega do pedido :

public class TransporteRodoviario : Transportador
{
  
public override void Transporte(Pedido pedido)
   {
     Console.WriteLine(
$"Pedido enviado via transporte rodoviário...");        
   }

   protected override bool IsAdequadoParaPedido(Pedido pedido)
   {
    
return !pedido.Urgente && pedido.Distancia < 1000;
   }
}
public class TransporteAereo : Transportador
{
  
public override void Transporte(Pedido pedido)
   {
     Console.WriteLine(
$"Pedido enviado via transporte aéreo...");     
   }
  
   protected
override bool IsAdequadoParaPedido(Pedido pedido)
   {
    
return pedido.Urgente || (pedido.Distancia >= 1000 && pedido.Peso <= 100);
   }
}
public class TransporteMaritimo : Transportador
{
  
public override void Transporte(Pedido pedido)
   {
     Console.WriteLine(
$"Pedido enviado via transporte maritimo...");  
   }
  
protected override bool IsAdequadoParaPedido(Pedido pedido)
   {
    
return !pedido.Urgente && (pedido.Distancia >= 1000 && pedido.Peso > 100);
   }
}

Portanto, precisamos usar a classe Transportador como uma fábrica que retorna uma classe Transportador adequada para nós, mas também deve fazer isso sem nenhuma condição codificada ou verificações manuais para poder adicionar novas classes  sem modificar códigos anteriores; para nos ajudar a fazer isso isso podemos usar Reflection.

Vamos começar escrevendo um método de extensão que retorna todas as classes derivadas de um determinado tipo.

Para isso vamos criar a classe estática Extensions e o método EncontraSubClasses :

public static class Extensions

  
public static IEnumerable<Type> EncontraSubClasses(this Type baseType)
   {
     
var assembly = baseType.Assembly;
     
return assembly.GetTypes().Where(t => t.IsSubclassOf(baseType));
   }
}

Queremos encontrar todas as subclasses de transportadores, executar seu método IsAdequadoParaPedido e retornar a instância adequada.

Então, vamos dar os retoques finais em nossa classe abstrata Transportador.

public abstract class Transportador : IDisposable
{
    public static Transportador GetTransportador(Pedido pedido)
    {
        var instance = GetInstanciaAdequada(typeof(Transportador).EncontraSubClasses(), pedido);
        return instance;
    }
    private static Transportador GetInstanciaAdequada(IEnumerable<Type> types, Pedido pedido)
    {
        foreach (var @class in types)
        {
            try
            {
                var instance = Activator.CreateInstance(@class) as Transportador;
                var isSuitable = instance.IsAdequadoParaPedido(pedido);
                if (isSuitable != true)
                {
                    instance.Dispose();
                    continue;
                }
                return instance;
            }
            catch 
            {
                continue;
            }
        }
        throw new NotImplementedException("Não foi possível encontrar um tipo
                                                     de transporte para o pedido: " + pedido);
    }
    public abstract void Transporte(Pedido pedido);
    protected abstract bool IsAdequadoParaPedido(Pedido pedido);
    public void Dispose()
    {
        GC.SuppressFinalize(this);
    }
}

Note que incluimos um método estático GetTransportador que obtém todas as subclasses do assembly e retorna uma delas que seja adequada para o pedido.

O método de fábrica não precisa criar novas instâncias o tempo todo. Ele também pode retornar objetos existentes de um cache, um pool de objetos ou outra fonte.

Como etapa final, vamos retornar ao arquivo Program.cs, preparar uma lista de pedidos e transportá-los.

using CSharp_Solid1;
List<Pedido> pedidos = new() { new Pedido { Urgente = false, Distancia = 100, Peso = 50 },
                               new Pedido { Urgente = false, Distancia = 2000, Peso = 4000 },
                               new Pedido { Urgente = false, Distancia = 1100, Peso = 5 },
                               new Pedido { Urgente = true,  Distancia = 1200, Peso = 250 }
};
foreach (var pedido in pedidos)
{
    Console.WriteLine("----------------------");
    Console.WriteLine(pedido.ToString());
    Transportador.GetTransportador(pedido).Transporte(pedido);
    Thread.Sleep(200);
}

Executando o projeto teremos o seguinte resultado:

E estamos conversados...  

Código do projeto : CSharp_Solid1.zip

"E é evidente que pela lei ninguém será justificado diante de Deus, porque o justo viverá pela fé."
Gálatas 3:11

Referências:


José Carlos Macoratti