SOLID

Princípio SOLID

Para começar vamos falar um pouco sobre os princípios SOLID. Esses princípios nasceram de um livro chamado ‘Agile Software Development: Principles, Patterns and Practices‘ escrito pelo Robert C. Martin, o famoso Uncle Bob, no ano de 2003. Assim, ele observou códigos escritos por diversos programadores e tentou criar um conjunto de regras que aumentaria a manitenibilidade e escalabilidade dos sistemas. Por fim ele organizou esses princípios no acrônimo SOLID.

Além disso esses conceitos se tornaram referência para a comunidade de desenvolvimento: Eles são utilizados como guia para criação de sistemas ajudando a aumentar a coesão e reduzindo o acoplamento neles.

Aqui no blog temos alguns outros artigos relacionados a design system como: DDD estratégico e DDD Tático. Eles podem ser uma leitura complementar interessante para esse tema.

SRP (Single Responsibility Principle) – Princípio da Responsabilidade única

Uma classe deve ter apenas uma única responsabilidade, ou seja, ela deve ter apenas uma razão para mudar. O motivo da classe existir deve ser muito claro e o motivo para alterar também. Não pode haver duas classes com o mesmo propósito mas nada impede de existir duas classes diferentes com o mesmo nome com propósitos distintos. Isso pode suar estranho mas nem sempre é. Em algumas organizações determinados termos utilizados pelos setores variam de significado.

// A única função da classe Pedido é efetuar pedidos
public class Pedido
{
    public int Id { get; set; }
    public DateTime Data { get; set; }
    public decimal Total { get; set; }
    public List<ItemPedido> Itens { get; set; }
}
// Um relatório de vendas não pertence a um pedido e, por isso, outra classe deve ser responsável por isso
public class RelatorioVendas
{
    public void GerarRelatorioDiario(List<Pedido> pedidos)
    {
        // código para gerar o relatório de vendas diárias com base nos pedidos recebidos
    }
}

OCP (Open/Closed Principle) – Princípio Aberto/Fechado

As classes devem estar abertas para extensão, mas fechadas para modificação: Essa frase é simples até demais, gerando certa confusão. A ideia é que a classe deve estar suficientemente bem estruturada para que novas melhorias não necessitem alterar a classe em si. Normalmente se utiliza polimorfismo para suportar esse princípio.

// Ao criar a interface IPagamento é possível realizar pagamento de diversas maneiras diferentes. 
// Se houvesse apenas uma classe pagamento sem isso ela poderia ter problemas
public interface IPagamento
{
    void RealizarPagamento(decimal valor);
}
// Nesse exemplo a classe pagamento implementa o Pattern Strategy
// Ele realiza pagamento de acordo com uma interface que possui implementações particulares
public class Pagamento
{
    public void ProcessarPagamento(IPagamento formaDePagamento, decimal valor)
    {
        formaDePagamento.RealizarPagamento(valor);
    }
}

public class PagamentoCartaoCredito : IPagamento
{
    public void RealizarPagamento(decimal valor)
    {
        // código para processar o pagamento via cartão de crédito
    }
}

public class PagamentoCartaoDebito : IPagamento
{
    public void RealizarPagamento(decimal valor)
    {
        // código para processar o pagamento via cartão de débito
    }
}

public class PagamentoBoleto : IPagamento
{
    public void RealizarPagamento(decimal valor)
    {
        // código para processar o pagamento via boleto
    }
}

LSP (Liskov Substitution Principle) – Princípio da substituição de Liskov

Já o princípio da Substituição de Liskov afirma que um objeto de uma classe derivada deve ser substituível por um objeto de sua classe base sem quebrar a integridade do sistema. Pense que uma classe que herda de outra terá os métodos da superclasse e, por conta disso, eles devem fazer sentido na classe filha.

// Superclasse simpels que identifica o saldo de uma conta bancária
// Ele pode ser positivo ou negativo
public class ContaBancaria
{
    public decimal Saldo { get; private set; }

    public void Depositar(decimal valor)
    {
        Saldo += valor;
    }

    public virtual void Sacar(decimal valor)
    {
        Saldo -= valor;
    }
}
// Uma classe de conta poupança não pode admitir valores negativos para saldo
// Uma implementação possível é a sobreescrita do método de saque
public class ContaPoupanca : ContaBancaria
{
    public override void Sacar(decimal valor)
    {
        if (Saldo - valor < 0)
        {
            throw new InvalidOperationException("Saldo insuficiente");
        }

        Saldo -= valor;
    }
}

ISP (Interface Segregation Principle) – Princípio da segregação de interfaces

Ele define que uma classe não deve ser forçada a implementar interfaces que não usa. Em vez disso, ela deve implementar apenas as interfaces que são relevantes para sua funcionalidade. Isso pode fazer com que uma interface com muito métodos tenha que ser dividida em várias interfaces com poucos métodos. Caso contrário uma classe terá métodos que não fazem sentido e o sistema estará tecnicamente inconsistente.

Para exemplificar, primeiro vamos demonstrar como não deve ser feito:

// Interface geral que indica como deve ser a movimentação do estoque
public interface IMovimentacaoEstoque
{
    void Adicionar(ItemEstoque item);
    void Remover(ItemEstoque item);
    void Mover(ItemEstoque item, LocalEstoque destino);
}

// Classe especializada na remoção do estoque
// Ela implementa incorretamente a interface IMovimentacaoEstoque
public class RemocaoEstoque: IMovimentacaoEstoque
{
    public void Adicionar(ItemEstoque item)
    {
         throw new Exception("Método inexistente");
    }

    public void Remover(ItemEstoque item)
    {
         // Implementação da remoção do estoque
    }

    public void Mover(ItemEstoque item, LocalEstoque destino)
    {
         throw new Exception("Método inexistente");
    }
}

Esse exemplo demonstra claramente que ao implementar a interface com dois métodos a mais a classe RemocaoEstoque ficou operando de modo inconsistente. Vamos a um outro exemplo, mas programado corretamento sob o ISP:

// Interface específica para remoção do estoque
public interface IRemocaoEstoque
{
    void Remover(ItemEstoque item);
    void Descartar(ItemEstoque item);
}

// A interface de movimentação considera ter também os métodos da remoção
public interface IMovimentacaoEstoque: IRemocaoEstoque
{
    void Mover(ItemEstoque item, LocalEstoque destino);
}
// A Classe concreta tem apenas as opções adequadas
public class RemocaoEstoque : IRemocaoEstoque
{
    public void Remover(ItemEstoque item)
    {
        // Implementação da remoção de estoque
    }

    public void Descartar(ItemEstoque item)
    {
        // Implementação do descarte de estoque
    }
}

DIP (Dependency Inversion Principle) – Princípio da inversão de dependência

Princípio da Inversão de Dependência, que afirma que os módulos de alto nível não devem depender de módulos de baixo nível. Em vez disso, ambos devem depender de abstrações. Para esse princípio é importante utilizar classes que implementem uma abstração comum (seja interface ou classe abstrata) e uma outra classe provê o acesso às classes sem precisar saber quais são elas.

// Essa é a interface IDataStorage utilizada como referência para o exemplo
public interface IDataStorage
{
    string GetData(string key);
    void SaveData(string key, string value);
    void DeleteData(string key);
}
// Veja 3 diferentes classes que implementam IDataStorage
public class DatabaseStorage : IDataStorage
{
    public string GetData(string key)
    {
        // Lógica para obter dados do banco de dados
    }

    public void SaveData(string key, string value)
    {
        // Lógica para salvar dados no banco de dados
    }

    public void DeleteData(string key)
    {
        // Lógica para excluir dados do banco de dados
    }
}

public class FileStorage : IDataStorage
{
    public string GetData(string key)
    {
        // Lógica para obter dados de um arquivo
    }

    public void SaveData(string key, string value)
    {
        // Lógica para salvar dados em um arquivo
    }

    public void DeleteData(string key)
    {
        // Lógica para excluir dados de um arquivo
    }
}

public class CacheStorage : IDataStorage
{
    public string GetData(string key)
    {
        // Lógica para obter dados do cache
    }

    public void SaveData(string key, string value)
    {
        // Lógica para salvar dados no cache
    }

    public void DeleteData(string key)
    {
        // Lógica para excluir dados do cache
    }
}
// Por fim, essa é a classe DataService que recebe um IDataStorage como parâmetro e opera suas ações.
// O serviço não sabe concretamente qual é o IDataStorage utilizado
public class DataService
{
    private readonly IDataStorage _dataStorage;

    public DataService(IDataStorage dataStorage)
    {
        _dataStorage = dataStorage;
    }

    public string GetData(string key)
    {
        return _dataStorage.GetData(key);
    }

    public void SaveData(string key, string value)
    {
        _dataStorage.SaveData(key, value);
    }

    public void DeleteData(string key)
    {
        _dataStorage.DeleteData(key);
    }
}

Conclusão sobre SOLID

Entender bem os princípios SOLID é algo absolutamente fundamental nos dias atuais. O artigo SOLID fala um pouco sobre cada um dos padrões com exemplos reais, e não os de faculdade, para melhor compreensão do tema.


Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário