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);
|
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: