C# - Violações dos princípios SOLID - II
Hoje veremos violações dos principíos SOLID que fazem parte do dia a dia de todo o desenvolvedor. |
Continuando o artigo anterior veremos agora a violação do princípio SOLID Open-Closed.
O princípio Aberto-Fechado (Open-Closed Principle, em inglês) é um dos princípios SOLID e prega que "entidades de software devem ser abertas para extensão, mas fechadas para modificação". Isso significa que um código deve ser projetado de forma que possa ser facilmente estendido para incluir novos comportamentos ou recursos, sem precisar modificar o código existente.
Violações
do princípio Open-Closed
No entanto, no dia a dia do desenvolvedor, algumas violações
comuns do princípio Open-Closed
podem ocorrer, como:
Como exemplo prático, suponha que temos um sistema de pagamento que pode lidar com diferentes tipos de pagamento, como cartão de crédito, PayPal, boleto bancário, etc.
Uma maneira comum de implementar isso seria criar uma classe base ModoPagamento e, em seguida, criar subclasses para cada tipo de pagamento, como CreditCardPagamento, PayPalPagamento e BoletoPagamento. Cada uma dessas subclasses implementaria a lógica específica para seu tipo de pagamento.
No entanto, se o código for modificado para adicionar um novo tipo de pagamento, como um método de pagamento por transferência bancária, por exemplo, pode ocorrer uma violação do Princípio Aberto-Fechado. Um desenvolvedor pode decidir simplesmente adicionar um novo método de pagamento à classe ModoPagamento, em vez de criar uma nova subclasse. O resultado seria um método de pagamento único e gigantesco, que seria difícil de estender ou modificar no futuro.
Exemplo:
public class ModoPagamento
{
public virtual void ProcessarPagamento(double valor)
{
// código para processar o pagamento
}
}
public class CreditCardPagamento : ModoPagamento
{
public override void ProcessarPagamento(double valor)
{
// código específico para processar pagamento com cartão de crédito
}
}
public class PayPalPagamento : ModoPagamento
{
public override void ProcessarPagamento(double valor)
{
// código específico para processar pagamento com PayPal
}
}
// Nova funcionalidade adicionada diretamente na classe PaymentMethod
public class TransferenciaBancariaPagamento : ModoPagamento
{
public override void ProcessarPagamento(double valor)
{
// código específico para processar pagamento por transferência bancária
}
}
|
Como podemos ver, a nova funcionalidade de pagamento por transferência bancária foi adicionada diretamente à classe ModoPagamento, em vez de criar uma nova subclasse para ela. Isso viola o Princípio Aberto-Fechado, pois o código existente foi modificado em vez de ser estendido.
Para corrigir essa violação, seria necessário criar uma nova subclasse específica para a transferência bancária. Dessa forma, o código existente não precisaria ser modificado e seria mais fácil de estender no futuro.
Padrões que podem violar o princípio
Embora existam muitos padrões de projeto que são consistentes com o princípio SOLID open-closed, há alguns que podem violá-lo. Alguns exemplos incluem:
É importante ressaltar que esses padrões de projeto não são necessariamente ruins e, em muitos casos, podem ser úteis e eficazes. O importante é reconhecer que eles podem violar o princípio OCP em certas situações e tomar medidas para mitigar esse risco.
Como exemplo vou mostrar uma violação do princípio Open-Closed pelo padrão Decorator.
Violação do princípio pelo padrão Decorator
Considere o seguinte cenário:
1- Temos uma classe base Pizza que representa uma pizza simples.
2- Queremos permitir que os clientes possam adicionar coberturas adicionais à sua pizza, como queijo extra ou pepperoni.
3- Em vez de modificar a classe Pizza existente para suportar essas coberturas adicionais, podemos usar o padrão Decorator para estender a funcionalidade de uma pizza em tempo de execução.
Aqui está uma possível implementação do padrão Decorator para adicionar coberturas à nossa classe Pizza:
// Classe base para pizza simples
public abstract class Pizza
{
public abstract string GetDescription();
public abstract double GetCost();
}
// Decorator base que adiciona funcionalidades a uma pizza
public abstract class PizzaDecorator : Pizza
{
protected Pizza _pizza;
public PizzaDecorator(Pizza pizza)
{
_pizza = pizza;
}
public override string GetDescription()
{
return _pizza.GetDescription();
}
public override double GetCost()
{
return _pizza.GetCost();
}
}
// Decorator que adiciona queijo extra à pizza
public class CheeseDecorator : PizzaDecorator
{
public CheeseDecorator(Pizza pizza) : base(pizza) { }
public override string GetDescription()
{
return $"{_pizza.GetDescription()}, Extra Cheese";
}
public override double GetCost()
{
return _pizza.GetCost() + 1.50;
}
}
// Decorator que adiciona pepperoni à pizza
public class PepperoniDecorator : PizzaDecorator
{
public PepperoniDecorator(Pizza pizza) : base(pizza) { }
public override string GetDescription()
{
return $"{_pizza.GetDescription()}, Pepperoni";
}
public override double GetCost()
{
return _pizza.GetCost() + 2.00;
}
}
|
Nesse exemplo, a classe Pizza é a nossa classe base. As classes CheeseDecorator e PepperoniDecorator são nossos decoradores concretos que adicionam funcionalidade à classe Pizza. Esses decoradores estendem a funcionalidade da classe Pizza em tempo de execução, sem modificar o código da classe Pizza existente.
No entanto, o problema com essa implementação é que, se quisermos adicionar novas funcionalidades às nossas pizzas, como cogumelos ou cebolas, teríamos que modificar a hierarquia de classes do Decorator, adicionando uma nova classe de decorador para cada nova funcionalidade que desejamos adicionar. Isso viola o princípio Open-Closed, que afirma que as classes devem ser abertas para extensão, mas fechadas para modificação.
Portanto, essa implementação do padrão Decorator viola o princípio Open-Closed, pois requer que a hierarquia de classes do Decorator seja modificada para adicionar novas funcionalidades. Para resolver esse problema, poderíamos considerar uma abordagem alternativa, como usar uma lista de ingredientes para representar as coberturas da pizza, em vez de usar uma hierarquia de classes de decorador.
public class Pizza
{
public List<string> Ingredients { get; } = new List<string>();
public void AddIngredient(string ingredient)
{
Ingredients.Add(ingredient);
}
public double GetCost()
{
return 5.00 + Ingredients.Count * 1.50;
}
public string GetDescription()
{
return $"Pizza with {string.Join(", ", Ingredients)}";
}
}
|
Nessa implementação, a classe Pizza contém uma lista de ingredientes e métodos para adicionar ingredientes, calcular o custo total da pizza e obter uma descrição da pizza. A lista de ingredientes pode ser usada para representar as coberturas adicionais que um cliente pode adicionar à sua pizza.
Aqui está um exemplo de como podemos criar e personalizar uma pizza usando essa abordagem:
var
pizza = new
Pizza(); pizza.AddIngredient("Tomato sauce"); pizza.AddIngredient("Cheese"); Console.WriteLine(pizza.GetDescription()); // "Pizza co tomate, molho e queijo Console.WriteLine(pizza.GetCost()); // 8.00 pizza.AddIngredient( "Pepperoni");// "pizza com molho de tomate, queijo e pepperoni Console.WriteLine(pizza.GetDescription()); Console.WriteLine(pizza.GetCost()); // 11.00 Console.ReadKey(); |
Nessa implementação, adicionar novas funcionalidades à classe Pizza, como cogumelos ou cebolas, é tão simples quanto adicionar um novo ingrediente à lista de ingredientes.
Essa abordagem é mais flexível e escalável do que a implementação anterior do padrão Decorator, que exigia a criação de uma nova classe de decorador para cada nova funcionalidade que queríamos adicionar.
Assim muitas vezes usar um padrão de projeto apenas por usar nem sempre te leva a melhor solução.
A seguir veremos as violações do principio LSP.
E estamos conversados ...
"Disse-lhes, pois, Jesus: Quando levantardes o Filho do homem, então
conhecereis que EU SOU, e que nada faço por mim mesmo; mas isto falo como meu
Pai me ensinou."
João 8:28
Referências:
LINQ - Gerando um Produto Cartesiano - Macoratti
C# - Salvando e Lendo informações em um arquivo XML - Macoratti