Os princípios SOLID são um conjunto de cinco diretrizes de design de software orientado a objetos, criadas por Robert C. Martin (Uncle Bob), que visam tornar o código mais compreensível, flexível e de fácil manutenção. Eles são fundamentais para o desenvolvimento de sistemas robustos e escaláveis, especialmente em projetos de médio e grande porte. Nesta aula, vamos explorar cada princípio, ver exemplos práticos em PHP e entender como aplicá-los no dia a dia.

Esses princípios não são regras rígidas, mas sim boas práticas que ajudam a evitar armadilhas comuns, como classes gigantes, acoplamento excessivo e dificuldade para testar. Ao dominar o SOLID, você estará apto a criar arquiteturas mais limpas e a evoluir seu código com segurança.

Cada princípio explicado

SOLID é um acrônimo que representa cinco princípios:

  • Single Responsibility Principle (SRP) — Princípio da Responsabilidade Única
  • Open/Closed Principle (OCP) — Princípio Aberto/Fechado
  • Liskov Substitution Principle (LSP) — Princípio da Substituição de Liskov
  • Interface Segregation Principle (ISP) — Princípio da Segregação de Interfaces
  • Dependency Inversion Principle (DIP) — Princípio da Inversão de Dependência

Cada princípio aborda um aspecto específico do design, e juntos eles promovem um código mais coeso, com baixo acoplamento e alta testabilidade. Vamos detalhar cada um deles.

1. Single Responsibility Principle (SRP)

O SRP afirma que uma classe deve ter um, e somente um, motivo para mudar. Em outras palavras, uma classe deve ter uma única responsabilidade. Isso não significa que ela deve fazer apenas uma coisa trivial, mas sim que todas as suas funcionalidades devem estar relacionadas a um único propósito. Por exemplo, uma classe que lida com persistência de dados não deve também ser responsável por gerar relatórios.

Quando uma classe acumula múltiplas responsabilidades, ela se torna mais difícil de entender, testar e modificar. Alterações em uma das responsabilidades podem impactar as outras, gerando bugs inesperados. O SRP ajuda a manter o código organizado e a reduzir o acoplamento.

Exemplo em PHP:

<?php

// Violação do SRP: a classe faz duas coisas
class Usuario {
    public function salvar(Usuario $usuario) {
        // lógica para salvar no banco
    }

    public function gerarRelatorio() {
        // lógica para gerar relatório do usuário
    }
}

// Respeitando o SRP: separamos as responsabilidades
class Usuario {
    // propriedades e métodos relacionados apenas ao usuário
}

class UsuarioRepository {
    public function salvar(Usuario $usuario) {
        // lógica para salvar no banco
    }
}

class RelatorioUsuario {
    public function gerar(Usuario $usuario) {
        // lógica para gerar relatório
    }
}

2. Open/Closed Principle (OCP)

O OCP estabelece que as entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação. Ou seja, você deve ser capaz de adicionar novos comportamentos sem alterar o código existente. Isso é geralmente alcançado por meio de herança, interfaces ou composição.

Se você modifica constantemente uma classe para adicionar novas funcionalidades, corre o risco de introduzir bugs em funcionalidades já testadas. O OCP promove a criação de código extensível, o que é fundamental em sistemas que evoluem rapidamente.

Exemplo em PHP:

<?php

// Violação do OCP: para adicionar novo tipo, precisamos modificar a classe
class CalculadoraDeDesconto {
    public function calcular(float $preco, string $tipo): float {
        if ($tipo === 'vip') {
            return $preco * 0.8;
        } elseif ($tipo === 'funcionario') {
            return $preco * 0.9;
        }
        return $preco;
    }
}

// Respeitando o OCP: usamos polimorfismo
interface Desconto {
    public function aplicar(float $preco): float;
}

class DescontoVip implements Desconto {
    public function aplicar(float $preco): float {
        return $preco * 0.8;
    }
}

class DescontoFuncionario implements Desconto {
    public function aplicar(float $preco): float {
        return $preco * 0.9;
    }
}

class CalculadoraDeDesconto {
    public function calcular(float $preco, Desconto $desconto): float {
        return $desconto->aplicar($preco);
    }
}

3. Liskov Substitution Principle (LSP)

O LSP estabelece que objetos de uma classe derivada devem ser substituíveis por objetos da classe base sem afetar a corretude do programa. Em outras palavras, uma subclasse deve ser capaz de substituir sua superclasse sem quebrar o comportamento esperado. Isso implica que a subclasse não deve reforçar pré-condições ou enfraquecer pós-condições.

Esse princípio é fundamental para o polimorfismo. Se uma subclasse não respeita o contrato da superclasse, o código que depende da superclasse pode falhar. O LSP garante que as hierarquias de herança sejam bem projetadas.

Exemplo em PHP:

<?php

// Violação do LSP: PatoDeBorracha não voa, mas herda de Ave
class Ave {
    public function voar() {
        echo "Voando\n";
    }
}

class Pato extends Ave {}

class PatoDeBorracha extends Ave {
    public function voar() {
        throw new Exception("Não posso voar!");
    }
}

// Respeitando o LSP: separamos as capacidades em interfaces
interface Voador {
    public function voar();
}

class Ave {}

class Pato extends Ave implements Voador {
    public function voar() {
        echo "Voando\n";
    }
}

class PatoDeBorracha extends Ave {}

4. Interface Segregation Principle (ISP)

O ISP afirma que nenhum cliente deve ser forçado a depender de métodos que não utiliza. Em vez de interfaces grandes e genéricas, é melhor criar interfaces menores e mais específicas. Isso evita que classes implementem métodos desnecessários, reduzindo o acoplamento e aumentando a coesão.

Quando uma interface é muito abrangente, as classes que a implementam precisam implementar todos os métodos, mesmo aqueles que não fazem sentido para elas. Isso gera código morto e dificulta a manutenção.

Exemplo em PHP:

<?php

// Violação do ISP: interface muito grande
interface Trabalhador {
    public function trabalhar();
    public function comer();
    public function dormir();
}

class Robo implements Trabalhador {
    public function trabalhar() {}
    public function comer() { throw new Exception("Robô não come"); }
    public function dormir() { throw new Exception("Robô não dorme"); }
}

// Respeitando o ISP: interfaces específicas
interface Trabalhador {
    public function trabalhar();
}

interface Humano {
    public function comer();
    public function dormir();
}

class Robo implements Trabalhador {
    public function trabalhar() {}
}

class Pessoa implements Trabalhador, Humano {
    public function trabalhar() {}
    public function comer() {}
    public function dormir() {}
}

5. Dependency Inversion Principle (DIP)

O DIP estabelece que módulos de alto nível não devem depender de módulos de baixo nível, mas sim de abstrações. Além disso, abstrações não devem depender de detalhes, mas os detalhes devem depender de abstrações. Isso é alcançado por meio do uso de interfaces ou classes abstratas.

Esse princípio promove o baixo acoplamento e facilita a manutenção e os testes, pois é possível substituir implementações concretas por mocks ou outras implementações sem alterar o módulo de alto nível.

Exemplo em PHP:

<?php

// Violação do DIP: classe de alto nível depende de classe concreta
class EmailService {
    public function enviar(string $mensagem) {
        echo "Enviando email: $mensagem\n";
    }
}

class Notificacao {
    private EmailService $servico;

    public function __construct() {
        $this->servico = new EmailService();
    }

    public function enviar(string $mensagem) {
        $this->servico->enviar($mensagem);
    }
}

// Respeitando o DIP: dependemos de uma abstração
interface ServicoDeMensagem {
    public function enviar(string $mensagem);
}

class EmailService implements ServicoDeMensagem {
    public function enviar(string $mensagem) {
        echo "Enviando email: $mensagem\n";
    }
}

class Notificacao {
    private ServicoDeMensagem $servico;

    public function __construct(ServicoDeMensagem $servico) {
        $this->servico = $servico;
    }

    public function enviar(string $mensagem) {
        $this->servico->enviar($mensagem);
    }
}

Benefícios

A adoção dos princípios SOLID traz inúmeros benefícios para o desenvolvimento de software:

  • Manutenibilidade: Código organizado e modular é mais fácil de entender e modificar, reduzindo o custo de manutenção.
  • Flexibilidade: Novos comportamentos podem ser adicionados com pouco ou nenhum impacto no código existente, graças ao OCP e DIP.
  • Testabilidade: Com classes coesas e dependências invertidas, é mais simples escrever testes unitários, pois é possível isolar componentes.
  • Reutilização: Classes com responsabilidade única e interfaces específicas podem ser reutilizadas em diferentes contextos.
  • Redução de riscos: Alterações em uma parte do sistema têm menos probabilidade de introduzir bugs em outras partes.
  • Colaboração: Código limpo e bem estruturado facilita o trabalho em equipe, pois os desenvolvedores entendem melhor o sistema.

Quando aplicar

Os princípios SOLID são especialmente úteis em projetos de médio e grande porte, onde a complexidade tende a crescer. Contudo, eles também podem ser aplicados em projetos menores, desde que não se caia no excesso de abstração. Algumas orientações:

  • SRP: Aplique sempre que uma classe estiver acumulando múltiplas responsabilidades. É o princípio mais fácil de aplicar e traz ganhos imediatos.
  • OCP: Aplique quando você prevê que novas funcionalidades serão adicionadas com frequência, como em sistemas de plugins ou regras de negócio variáveis.
  • LSP: Aplique ao projetar hierarquias de herança. Verifique se as subclasses realmente substituem a superclasse sem quebrar o contrato.
  • ISP: Aplique quando você tem interfaces muito grandes e classes que precisam implementar métodos que não usam. Prefira interfaces menores e específicas.
  • DIP: Aplique quando você quer desacoplar módulos de alto nível de implementações concretas, especialmente em arquiteturas de camadas.

É importante equilibrar a aplicação dos princípios: não adote todos eles cegamente em qualquer situação, pois isso pode levar a um over-engineering. Avalie o contexto e a complexidade do projeto.

Boas práticas e observações finais

Algumas dicas para aplicar o SOLID de forma eficaz:

  • Comece aplicando o SRP em seu código atual; é o mais simples e traz benefícios imediatos.
  • Use interfaces e injeção de dependência para facilitar o DIP.
  • Prefira composição sobre herança sempre que possível, o que ajuda no LSP.
  • Não tenha medo de refatorar: os princípios SOLID são um guia, não uma obrigação dogmática.
  • Escreva testes para verificar que seus princípios estão sendo respeitados, especialmente o LSP.

Referências

Exercícios

  1. Identifique no código abaixo quais princípios SOLID estão sendo violados e explique por quê.

    class Pedido {
        public function calcularTotal() { /* ... */ }
        public function salvarNoBanco() { /* ... */ }
        public function enviarEmailConfirmacao() { /* ... */ }
    }

    ✓ Resposta: O código viola o SRP, pois a classe Pedido tem múltiplas responsabilidades: calcular o total, persistir no banco e enviar e-mail. Também pode violar o DIP, se as dependências forem criadas diretamente dentro da classe.
  2. Refatore o código abaixo para respeitar o Open/Closed Principle, permitindo adicionar novos métodos de pagamento sem modificar a classe ProcessadorPagamento.

    class ProcessadorPagamento {
        public function pagar(string $metodo, float $valor) {
            if ($metodo === 'cartao') { /* ... */ }
            elseif ($metodo === 'paypal') { /* ... */ }
            elseif ($metodo === 'boleto') { /* ... */ }
        }
    }

    ✓ Resposta: Podemos criar uma interface MetodoPagamento com o método pagar(float $valor) e classes concretas para cada método. O ProcessadorPagamento recebe a interface e chama o método pagar. Exemplo:
    interface MetodoPagamento {
        public function pagar(float $valor);
    }
    
    class Cartao implements MetodoPagamento {
        public function pagar(float $valor) { /* ... */ }
    }
    
    class PayPal implements MetodoPagamento {
        public function pagar(float $valor) { /* ... */ }
    }
    
    class Boleto implements MetodoPagamento {
        public function pagar(float $valor) { /* ... */ }
    }
    
    class ProcessadorPagamento {
        public function pagar(MetodoPagamento $metodo, float $valor) {
            $metodo->pagar($valor);
        }
    }
  3. Dado o seguinte código, explique como o Liskov Substitution Principle é violado e proponha uma correção.

    class Retangulo {
        protected $largura;
        protected $altura;
    
        public function setLargura($largura) { $this->largura = $largura; }
        public function setAltura($altura) { $this->altura = $altura; }
        public function area() { return $this->largura * $this->altura; }
    }
    
    class Quadrado extends Retangulo {
        public function setLargura($largura) {
            $this->largura = $largura;
            $this->altura = $largura;
        }
        public function setAltura($altura) {
            $this->altura = $altura;
            $this->largura = $altura;
        }
    }

    ✓ Resposta: A violação ocorre porque Quadrado altera o comportamento dos setters de Retangulo, quebrando a expectativa de que a largura e a altura podem ser definidas independentemente. Em um código que usa Retangulo, se você passar um Quadrado, o resultado da área pode ser inesperado. A correção é não herdar de Retangulo; usar composição ou criar uma classe base abstrata para formas geométricas.
  4. Considere a seguinte interface. Aplique o Interface Segregation Principle e mostre como ela pode ser dividida.

    interface OperacoesDeBanco {
        public function depositar(float $valor);
        public function sacar(float $valor);
        public function transferir(float $valor, string $destino);
        public function consultarSaldo();
    }

    ✓ Resposta: Podemos dividir em duas interfaces: uma para operações básicas (depositar, sacar, consultarSaldo) e outra para transferências. Exemplo:
    interface OperacoesBasicas {
        public function depositar(float $valor);
        public function sacar(float $valor);
        public function consultarSaldo();
    }
    
    interface Transferivel {
        public function transferir(float $valor, string $destino);
    }
  5. O código abaixo viola o Dependency Inversion Principle. Refatore-o para usar injeção de dependência.

    class PedidoRepositorio {
        public function salvar(Pedido $pedido) { /* ... */ }
    }
    
    class PedidoService {
        private PedidoRepositorio $repositorio;
    
        public function __construct() {
            $this->repositorio = new PedidoRepositorio();
        }
    
        public function processar(Pedido $pedido) {
            $this->repositorio->salvar($pedido);
            // outras operações
        }
    }

    ✓ Resposta: A classe PedidoService depende diretamente da classe concreta PedidoRepositorio. Para respeitar o DIP, criamos uma interface para o repositório e injetamos no construtor. Exemplo:
    interface PedidoRepositorioInterface {
        public function salvar(Pedido $pedido);
    }
    
    class PedidoRepositorio implements PedidoRepositorioInterface {
        public function salvar(Pedido $pedido) { /* ... */ }
    }
    
    class PedidoService {
        private PedidoRepositorioInterface $repositorio;
    
        public function __construct(PedidoRepositorioInterface $repositorio) {
            $this->repositorio = $repositorio;
        }
    
        public function processar(Pedido $pedido) {
            $this->repositorio->salvar($pedido);
            // outras operações
        }
    }