SOLID -  Resolvendo problemas com Design Patterns


  Hoje vamos recordar os princípios SOLID e também mostrar que podemos usar os Design Patterns para aplicar os princípios.

Os princípios SOLID e os Design Patterns são conceitos fundamentais na programação orientada a objetos. Entender como os padrões de projeto implementam os princípios SOLID ajuda desenvolvedores a criar aplicações manuteníveis e escaláveis.



Os princípios SOLID definem boas regras de design, enquanto os Design Patterns oferecem formas testadas de implementá-las.

- Princípios (SOLID) → dizem o que fazer
- Padrões (Patterns) → mostram como fazer


Neste guia, vamos aplicar isso com exemplos realistas no contexto de aplicações .NET.

1- Princípio da Responsabilidade Única - SRP com Strategy Pattern

SRP - Uma classe deve ter apenas um motivo para mudar.
Strategy Pattern - Permite encapsular diferentes algoritmos em classes separadas, tornando-os intercambiáveis.

Problema real

Uma classe de pagamento fazendo tudo (muitas responsabilidades)

public class ProcessadorPagamento
{
   public void ProcessarCartao(decimal valor) { }
   public void ProcessarPix(decimal valor) { }
   public void ProcessarBoleto(decimal valor) { }
}

Isso viola o SRP:  Cada método muda por um motivo diferente e cresce sem controle

Solução

Usando o Strategy

public interface IEstrategiaPagamento
{
    void Processar(decimal valor);
}
public class PagamentoCartao : IEstrategiaPagamento
{
    public void Processar(decimal valor)
    {
        // lógica cartão
    }
}
public class PagamentoPix : IEstrategiaPagamento
{
    public void Processar(decimal valor)
    {
        // lógica PIX
    }
}

Uso:

public class ServicoPagamento
{
    private readonly IEstrategiaPagamento _estrategia;
    public ServicoPagamento(IEstrategiaPagamento estrategia)
    {
        _estrategia = estrategia;
    }
    public void Executar(decimal valor)
    {
        _estrategia.Processar(valor);
    }
}

Por que isso é melhor ?
- Cada classe tem uma responsabilidade clara
- Facilita testes
- Permite extensão sem bagunçar código existente

2- OCP - Open/Close Principle com Decorator Pattern

OCP - Software deve estar aberto para extensão e fechado para modificação.
Decorator Pattern - Permite adicionar comportamento a um objeto dinamicamente sem alterar sua estrutura.

Problema real

Toda nova regra exige modificar a classe

public class Pedido
{
    public decimal CalcularTotal(bool adicionarFrete, bool aplicarDesconto)
    {
        decimal total = 100;
        if (adicionarFrete) total += 20;
        if (aplicarDesconto) total -= 10;
        return total;
    }
}

Solução com Decorator

public interface IPedido
{
    decimal CalcularTotal();
}
public class PedidoBase : IPedido
{
    public decimal CalcularTotal() => 100;
}

Usando o Decorator:

public class FreteDecorator : IPedido
{
    private readonly IPedido _pedido;
    public FreteDecorator(IPedido pedido)
    {
        _pedido = pedido;
    }
    public decimal CalcularTotal()
    {
        return _pedido.CalcularTotal() + 20;
    }
}

Outro:

public class DescontoDecorator : IPedido
{
    private readonly IPedido _pedido;
    public DescontoDecorator(IPedido pedido)
    {
        _pedido = pedido;
    }
    public decimal CalcularTotal()
    {
        return _pedido.CalcularTotal() - 10;
    }
}

Uso:

IPedido pedido = new PedidoBase();
pedido = new FreteDecorator(pedido);
pedido = new DescontoDecorator(pedido);

Benefícios:
-
Não altera código existente
- Temos uma Extensão limpa
- Composição sobre herança

3- LSP - Liskov Substitution Principle (sem forçar padrão)

LSP - Subtipos devem poder substituir seus tipos base sem quebrar o comportamento esperado.

Problema clássico

Isso quebra o contrato

public class Ave
{
    public virtual void Voar() { }
}
public class Pinguim : Ave
{
    public override void Voar()
    {
        throw new Exception("Não voa");
    }
}

Solução

public interface IAve
{
    void Mover();
}

public interface IAveQueVoa
{
   void Voar();
}

SImplementações

public class Pardal : IAve, IAveQueVoa
{
    public void Mover() => Voar();
    public void Voar() { }
}
public class Pinguim : IAve
{
    public void Mover() => Nadar();
    private void Nadar() { }
}

Aqui não tem mágica, o  LSP é resolvido com modelagem correta, não com padrão específico

4- ISP - Interface Segregation com Adapter Pattern

ISP - Clientes não devem ser forçados a depender de métodos que não usam.
Adapter - Permite que interfaces incompatíveis trabalhem juntas.

Problema real

Nem todo cliente precisa dos dois

public interface IRelatorio
{
    void GerarPdf();
    void GerarExcel();
}

Solução: ISP Aplicado

public interface IRelatorioPdf
{
    void GerarPdf();
}
public interface IRelatorioExcel
{
    void GerarExcel();
}

Adapter em cenário real

Sistema Legado:

public class SistemaLegado
{
    public void GerarArquivo(byte[] dados) { }
}

Usando o Adpater

public interface IGeradorRelatorio
{
    void Gerar(string conteudo);
}
public class AdaptadorLegado : IGeradorRelatorio
{
    private readonly SistemaLegado _legado;
    public AdaptadorLegado(SistemaLegado legado)
    {
        _legado = legado;
    }
    public void Gerar(string conteudo)
    {
        var bytes = Encoding.UTF8.GetBytes(conteudo);
        _legado.GerarArquivo(bytes);
    }
}

O princípio ISP e padrão Adapter não são dependentes, mas se complementam bem em cenários reais

5- DIP - Dependency Inversion + Dependency Injection

DIP - Dependa de abstrações, não de implementações concretas.
DI - Técnica para fornecer dependências externamente.

Problema real

Forte Acoplamento

public class ServicoNotificacao
{
     private ServicoEmail _email = new ServicoEmail();
}

Solução

public interface IServicoMensagem
{
     void Enviar(string mensagem); 
}
public class ServicoEmail : IServicoMensagem
{
   public void Enviar(string mensagem)
   {
     Console.WriteLine(mensagem);
   }
}	
public class ServicoNotificacao
{
   private readonly IServicoMensagem _servico;

   public ServicoNotificacao(IServicoMensagem servico)
   {
     _servico = servico;
   }

   public void Notificar(string mensagem)
   {
     _servico.Enviar(mensagem);
   }
}

Benefícios:
Baixo acoplamento
Alta testabilidade
Fácil substituição de implementação

Podemos melhorar este código usando Primary Constructor:

public class ServicoNotificacao(IServicoMensagem servico)
{
   public void Notificar(string mensagem)
   {
     servico.Enviar(mensagem);
    }
}

O que mudou na prática?
- Removeu o campo _servico
- Removeu o construtor explícito
- Código ficou mais direto e enxuto

Mas a essência continua:

A classe depende de abstração (IServicoMensagem)
A dependência continua sendo injetada
O DIP continua sendo respeitado

Aqui cabe destacar o escopo da variável servico :

O parâmetro servico:  Existe como campo implícito mas não aparece como _servico.

Evite usar Primary Constructor quando:
- Precisar de validação complexa no construtor
- Precisar inicializar múltiplos campos com lógica
- Desejar clareza explícita para iniciantes

E estamos conversados...  

"Àquele que não conheceu pecado (Jesus), o fez pecado por nós; para que nele fôssemos feitos justiça de Deus. 2 Coríntios 5:21

Referências:


José Carlos Macoratti