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:

  1. Adicionar novas funcionalidades alterando o código existente: em vez de criar uma nova classe ou módulo para lidar com a nova funcionalidade, alguns desenvolvedores podem optar por simplesmente adicionar o novo código ao existente. Isso pode criar acoplamento desnecessário entre as diferentes partes do código e torná-lo mais difícil de manter e estender no futuro.
     
  2. Monolitos grandes e complexos: um monolito é um aplicativo que é construído como um único bloco de código. Quando o aplicativo se torna grande e complexo, é difícil estender ou adicionar novos recursos sem modificar o código existente. Isso pode ser resolvido dividindo o aplicativo em módulos menores, cada um responsável por uma parte específica do aplicativo.
     
  3. Código repetido: muitas vezes, os desenvolvedores copiam e colam o código existente para criar um novo recurso ou comportamento, em vez de criar uma nova classe ou módulo. Isso pode levar a código repetido e dificultar a manutenção e extensão do código no futuro.
     
  4. Dependências acopladas: se uma classe ou módulo depende diretamente de outro, quaisquer mudanças em um deles podem afetar o outro. Isso pode tornar o código difícil de modificar e extender no futuro.

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:

  1. Template Method - O padrão de projeto Template Method permite que subclasses substituam partes do comportamento de um algoritmo sem alterar sua estrutura geral. No entanto, isso requer que as subclasses modifiquem a implementação de um método em uma classe abstrata, o que pode violar o princípio OCP se a classe abstrata precisar ser modificada para adicionar novos comportamentos.
     
  2. Visitor - O padrão de projeto Visitor permite adicionar novas operações a uma estrutura de objetos sem modificar as classes desses objetos. No entanto, isso requer que as classes dos objetos aceitem um visitante em sua interface, o que pode violar o princípio OCP se novas operações exigirem uma nova interface na classe do objeto.
     
  3. Bridge - O padrão de projeto Bridge permite que uma abstração e sua implementação variem independentemente. No entanto, isso pode violar o princípio OCP se novas variações na abstração ou na implementação exigirem modificações na classe Bridge.
     
  4. Decorator - O padrão de projeto Decorator permite que os objetos sejam estendidos com novas funcionalidades em tempo de execução, sem modificar o código existente. No entanto, isso pode violar o princípio OCP se a hierarquia de classes do Decorator precisar ser modificada para adicionar novas funcionalidades.

É 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:


José Carlos Macoratti