C# - Violações dos princípios SOLID - V


 Hoje vamos continuar mostrando as violações dos principíos SOLID que fazem parte do dia a dia de todo o desenvolvedor.

Continuando o artigo anterior veremos a violação do princípio SOLID DIP.

O princípio SOLID DIP (Dependency Inversion Principle) é um dos cinco princípios SOLID, que se refere à inversão de dependência. O objetivo desse princípio é garantir que as classes de um sistema sejam altamente coesas e fracamente acopladas.

A ideia central do DIP é que os módulos de um sistema devem depender de abstrações e não de implementações concretas. Isso significa que as classes de nível superior não devem depender diretamente das classes de nível inferior, mas sim de interfaces ou classes abstratas que representam essas classes de nível inferior.

Isso permite que as classes de nível superior sejam independentes das classes de nível inferior, tornando o sistema mais flexível e fácil de manter. Além disso, a inversão de dependência também permite a fácil substituição de uma classe de nível inferior por outra, desde que ambas implementem a mesma interface ou classe abstrata.

Violação do princípio DIP

Uma violação comum do princípio DIP é quando as classes de nível superior dependem diretamente das classes de nível inferior, em vez de depender de abstrações como interfaces ou classes abstratas. Isso ocorre quando as classes de nível superior instanciam diretamente as classes de nível inferior ou passam essas classes como parâmetros de método.

Por exemplo, imagine uma classe chamada ProcessaPedido que processa pedidos em um sistema de comércio eletrônico. Essa classe pode depender diretamente da classe DatabaseConnection para se conectar ao banco de dados e recuperar informações do pedido. Isso violaria o princípio DIP, pois ProcessaPedido estaria dependendo de uma implementação concreta, em vez de uma abstração.

Uma forma de corrigir essa violação seria criar uma interface ou classe abstrata para representar a conexão com o banco de dados, e fazer com que DatabaseConnection implemente essa abstração. Em seguida, ProcessaPedido dependeria dessa abstração em vez da implementação concreta. Isso permite que ProcessaPedido seja mais flexível e possa trabalhar com outras implementações de conexão de banco de dados no futuro.

A seguir temos o código que mostra essa violação do padrão:

// Implementação concreta da conexão com o banco de dados
public class DatabaseConnection 
{
    public void Connect()
    {
        // Código para estabelecer conexão com o banco de dados
    }
    public void Disconnect()
    {
        // Código para desconectar do banco de dados
    }
    // Implementação dos métodos para recuperar e salvar dados 
    // no banco de dados
}
// Classe ProcessarPedido que depende diretamente da implementação 
// concreta DatabaseConnection
public class ProcessarPedido 
{
    private DatabaseConnection _databaseConnection;
    public ProcessarPedido()
    {
        _databaseConnection = new DatabaseConnection();
    }
    public void Processar(Pedido pedido)
    {
        // Código para processar o pedido utilizando a conexão 
       //  com o banco de dados
        _databaseConnection.Connect();
        // ...
        _databaseConnection.Disconnect();
    }
}

Agora vejamos como fica o código acima aderente ao padrão DIP:

// Abstração para a conexão com o banco de dados
public interface IDatabaseConnection
{
    void Connect();
    void Disconnect();
    // Métodos para recuperar e salvar dados no banco de dados
}

// Implementação concreta da conexão com o banco de dados
public class DatabaseConnection : IDatabaseConnection
{
    public void Connect()
    {
        // Código para estabelecer conexão com o banco de dados
    }

    public void Disconnect()
    {
        // Código para desconectar do banco de dados
    }

    // Implementação dos métodos para recuperar e salvar
 dados no banco de dados

}

// Classe ProcessarPedido que depende da abstração IDatabaseConnection
public class ProcessarPedido
{
    private IDatabaseConnection _databaseConnection;

    public ProcessarPedido(IDatabaseConnection databaseConnection)
    {
        _databaseConnection = databaseConnection;
    }

    public void Processar(Pedido pedido)
    {
        // Código para processar o pedido utilizando a conexão
          com o banco de dados

        _databaseConnection.Connect();
        // ...
        _databaseConnection.Disconnect();
    }
}

Nesse exemplo, a classe ProcessarPedido depende da abstração IDatabaseConnection em vez da implementação concreta DatabaseConnection. Essa dependência é injetada por meio do construtor da classe, que recebe um objeto IDatabaseConnection como parâmetro.

Dessa forma, é possível passar diferentes implementações de IDatabaseConnection para ProcessarPedido, sem que a classe precise ser modificada. Isso torna o código mais flexível e fácil de manter.

É importante ressaltar que o princípio DIP não deve ser aplicado de forma dogmática, mas sim como uma orientação para melhorar a qualidade do código. Em alguns casos, pode ser necessário violar o DIP para atingir outros objetivos, como a desempenho. Nesses casos, é importante avaliar os prós e contras e escolher a melhor abordagem para cada situação específica.

Padrões que podem violar o princípio

Existem alguns padrões de projeto do GOF (Gang of Four) que, se implementados de forma inadequada, podem violar o princípio DIP (Dependency Inversion Principle). Alguns exemplos incluem:
  1. Singleton: O padrão Singleton é projetado para garantir que apenas uma instância de uma classe seja criada e que ela possa ser acessada globalmente. No entanto, se a implementação for feita de forma que uma classe de nível superior (ou cliente) dependa diretamente de uma classe Singleton, isso violará o princípio DIP, uma vez que a classe de nível superior estará dependendo diretamente da implementação concreta da classe Singleton.
     
  2. Factory Method: O padrão Factory Method é usado para definir uma interface para criar objetos, mas deixando as subclasses decidirem quais classes concretas instanciar. No entanto, se a classe cliente depender diretamente da classe Factory em vez de depender de uma interface, isso violará o princípio DIP, uma vez que a classe cliente estará dependendo diretamente da implementação concreta da classe Factory.
     
  3. Template Method: O padrão Template Method é usado para definir o esqueleto de um algoritmo, deixando algumas etapas para serem implementadas pelas subclasses. No entanto, se a classe cliente depender diretamente da classe abstrata que implementa o Template Method em vez de depender de uma interface, isso violará o princípio DIP, uma vez que a classe cliente estará dependendo diretamente da implementação concreta da classe abstrata.

É importante lembrar que esses padrões de projeto não são inerentemente ruins ou problemáticos, mas a forma como eles são implementados pode resultar em violações do princípio DIP.

Para evitar essas violações, é importante sempre depender de abstrações em vez de implementações concretas e utilizar a injeção de dependência para injetar as dependências de uma classe.

Exemplo de violação do princípio pelo padrão Template Method

Suponha que temos uma aplicação de gerenciamento de estoque que contém duas classes: Product e Order. A classe Product representa um produto no estoque e a classe Order representa uma ordem de compra.
 
public class Product
{
    public string Name { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}
public class Order
{
    private readonly List<Product> _products = new List<Product>();
    public void AddProduct(Product product)
    {
        _products.Add(product);
    }
    public decimal CalculateTotalPrice()
    {
        decimal totalPrice = 0;
        foreach (var product in _products)
        {
            totalPrice += product.Price;
        }
        return totalPrice;
    }
    public void PrintOrderDetails()
    {
        Console.WriteLine("Order details:");
        foreach (var product in _products)
        {
            Console.WriteLine($"Product: {product.Name}, Quantity: {product.Quantity}, 
                                     Price: {product.Price}");
        }
        Console.WriteLine($"Total Price: {CalculateTotalPrice()}");
    }
}

Agora, suponha que queremos adicionar uma nova funcionalidade à nossa aplicação, que é gerar um relatório de estoque. Para isso, podemos criar uma classe StockReport que usa o padrão Template Method para gerar o relatório.

A classe StockReport define um esqueleto de algoritmo e deixa algumas etapas para serem implementadas pelas subclasses.

public abstract class StockReport
{
    private readonly List<Product> _products = new List<Product>();
    public void GenerateReport()
    {
        LoadProducts();
        SortProducts();
        FormatReport();
        PrintReport();
    }
    protected abstract void LoadProducts();
    protected virtual void SortProducts()
    {
        _products.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));
    }
    protected abstract void FormatReport();
    protected virtual void PrintReport()
    {
        Console.WriteLine("Stock Report:");
        foreach (var product in _products)
        {
            Console.WriteLine($"Product: {product.Name}, Quantity:
 {product.Quantity}, Price: {product.Price}");
        }
    }
    protected void AddProduct(Product product)
    {
        _products.Add(product);
    }
}
public class InMemoryStockReport : StockReport
{
    private readonly List<Product> _productsInMemory;
    public InMemoryStockReport(List<Product> productsInMemory)
    {
        _productsInMemory = productsInMemory;
    }
    protected override void LoadProducts()
    {
        foreach (var product in _productsInMemory)
        {
            AddProduct(product);
        }
    }
    protected override void FormatReport()
    {
        var report = new StringBuilder();

        report.AppendLine("Stock Report:");

        foreach (var product in _products)
        {
            report.AppendLine($"Product: {product.Name}, Quantity: {product.Quantity}, 
Price: {product.Price}");
        }

        File.WriteAllText("stock_report.txt", report.ToString());
    }
}

Agora, podemos criar uma instância da classe InMemoryStockReport e gerar o relatório:

var products = new List<Product>
{
  
new Product { Name = "Product A", Quantity = 10, Price = 5.99M },
  
new Product { Name = "Product B", Quantity = 5, Price = 10.99M },
  
new Product { Name = "Product C", Quantity = 2, Price = 19.99M },
};


var
stockReport = new InMemoryStockReport(products);

Console.ReadKey();
 

Se continuarmos implementando a funcionalidade para gerar o relatório de estoque dentro da classe InMemoryStockReport, estaremos violando o Princípio da Inversão de Dependência (DIP), porque a classe InMemoryStockReport dependerá diretamente da implementação concreta da classe Product.

Observe que a classe InMemoryStockReport usa diretamente a classe Product para formatar o relatório e gravá-lo em um arquivo. Isso significa que, se a classe Product mudar, a classe InMemoryStockReport precisará ser alterada também. Essa dependência direta é uma violação do Princípio da Inversão de Dependência (DIP).

Para corrigir essa violação, podemos usar uma interface IProduct para representar um produto e injetá-la na classe InMemoryStockReport por meio do construtor. Dessa forma, a classe InMemoryStockReport dependerá apenas da interface IProduct, em vez de depender diretamente da implementação concreta da classe Product.

using System.Text;
public interface IProduct
{
    string Name { get; }
    int Quantity { get; }
    decimal Price { get; }
}
public class Product : IProduct
{
    public string Name { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}
public class InMemoryStockReport : StockReport
{
    private readonly List<IProduct> _productsInMemory;
    public InMemoryStockReport(List<IProduct> productsInMemory)
    {
        _productsInMemory = productsInMemory;
    }
    protected override void LoadProducts()
    {
        foreach (var product in _productsInMemory)
        {
            AddProduct(product);
        }
    }
    protected override void FormatReport()
    {
        var report = new StringBuilder();
        report.AppendLine("Stock Report:");
        foreach (var product in _products)
        {
            report.AppendLine($"Product: {product.Name}, Quantity: {product.Quantity}, 
Price: {product.Price}");
        }
        File.WriteAllText("stock_report.txt", report.ToString());
    }
}
public abstract class StockReport
{
    protected readonly List<IProduct> _products = new List<IProduct>();
    protected abstract void LoadProducts();
    protected abstract void FormatReport();
    public void Generate()
    {
        LoadProducts();
        FormatReport();
    }
    protected void AddProduct(IProduct product)
    {
        _products.Add(product);
    }
}

Observe que a classe abstrata StockReport agora depende apenas da interface IProduct e a classe InMemoryStockReport implementa essa interface.

Isso significa que a classe InMemoryStockReport pode ser facilmente substituída por outra classe que também implementa a interface IProduct, sem afetar a classe StockReport.

E estamos conversados ...

Disse Jesus : "Eu sou o bom Pastor; o bom Pastor dá a sua vida pelas ovelhas."
João 10:11

Referências:


José Carlos Macoratti