SOLID é um acrônimo para cinco princípios de design orientado a objetos formulados por Robert C. Martin. Eles não são regras rígidas — são diretrizes para escrever código que resiste ao tempo: fácil de estender, difícil de quebrar acidentalmente, e que comunica sua intenção com clareza.
Cada princípio resolve um problema específico que aparece quando sistemas crescem. Neste artigo, cada princípio é apresentado com um exemplo problemático e a versão corrigida — a diferença de qualidade fica imediatamente visível no código.
S — Single Responsibility Principle
Uma classe deve ter apenas um motivo para mudar. Se uma classe faz mais de uma coisa, mudanças em qualquer uma dessas coisas podem quebrar as outras — e os testes ficam mais difíceis de escrever.
<?php
declare(strict_types=1);
// ── PROBLEMA: UserManager faz tudo ───────────────────────────────────
class UserManager
{
public function criarUsuario(array $dados): void
{
// 1. Valida dados
if (empty($dados['email'])) throw new \InvalidArgumentException('Email obrigatório.');
// 2. Salva no banco
$pdo = new \PDO('mysql:host=localhost;dbname=app', 'root', '');
$pdo->prepare('INSERT INTO usuarios (email) VALUES (?)')->execute([$dados['email']]);
// 3. Envia email de boas-vindas
mail($dados['email'], 'Bem-vindo!', 'Sua conta foi criada.');
// 4. Registra log
file_put_contents('app.log', "Usuário criado: {$dados['email']}\n", FILE_APPEND);
}
// Motivos para mudar: validação, acesso a dados, email, log — são 4 motivos
}
// ── SOLUÇÃO: cada classe com uma responsabilidade ─────────────────────
class ValidadorUsuario
{
public function validar(array $dados): void
{
if (empty($dados['email']) || !filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email inválido.');
}
if (empty($dados['nome'])) {
throw new \InvalidArgumentException('Nome obrigatório.');
}
}
}
class UsuarioRepositorio
{
public function __construct(private readonly \PDO $pdo) {}
public function salvar(array $dados): int
{
$stmt = $this->pdo->prepare('INSERT INTO usuarios (nome, email) VALUES (?, ?)');
$stmt->execute([$dados['nome'], $dados['email']]);
return (int) $this->pdo->lastInsertId();
}
}
class EmailDeBoasVindas
{
public function enviar(string $email, string $nome): void
{
// Em produção: usa biblioteca de email (PHPMailer, Symfony Mailer)
mail($email, 'Bem-vindo!', "Olá {$nome}, sua conta foi criada.");
}
}
// Orquestrador — usa as peças, mas não as implementa
class RegistroDeUsuarioService
{
public function __construct(
private readonly ValidadorUsuario $validador,
private readonly UsuarioRepositorio $repositorio,
private readonly EmailDeBoasVindas $emailBoasVindas,
) {}
public function registrar(array $dados): int
{
$this->validador->validar($dados);
$id = $this->repositorio->salvar($dados);
$this->emailBoasVindas->enviar($dados['email'], $dados['nome']);
return $id;
}
}
O — Open/Closed Principle
Classes devem ser abertas para extensão e fechadas para modificação. Quando um novo requisito chega, você adiciona código — não modifica o que já funciona e está testado.
<?php
declare(strict_types=1);
// ── PROBLEMA: if/switch que cresce a cada novo tipo ───────────────────
class CalculadoraDeDesconto
{
public function calcular(string $tipoCLiente, float $total): float
{
if ($tipoCliente === 'vip') {
return $total * 0.20;
} elseif ($tipoCliente === 'fidelidade') {
return $total * 0.10;
} elseif ($tipoCliente === 'funcionario') {
return $total * 0.30;
}
// Para cada novo tipo, modifica esta classe — viola OCP
return 0.0;
}
}
// ── SOLUÇÃO: polimorfismo — nova regra = nova classe ──────────────────
interface RegraDeDesconto
{
public function calcular(float $total): float;
public function aplicavelA(string $tipoCliente): bool;
}
class DescontoVip implements RegraDeDesconto
{
public function calcular(float $total): float { return $total * 0.20; }
public function aplicavelA(string $t): bool { return $t === 'vip'; }
}
class DescontoFidelidade implements RegraDeDesconto
{
public function calcular(float $total): float { return $total * 0.10; }
public function aplicavelA(string $t): bool { return $t === 'fidelidade'; }
}
class DescontoFuncionario implements RegraDeDesconto
{
public function calcular(float $total): float { return $total * 0.30; }
public function aplicavelA(string $t): bool { return $t === 'funcionario'; }
}
// Calculadora fechada para modificação — aberta para extensão via injeção
class CalculadoraDeDescontoOCP
{
/** @param RegraDeDesconto[] $regras */
public function __construct(private readonly array $regras = []) {}
public function calcular(string $tipoCliente, float $total): float
{
foreach ($this->regras as $regra) {
if ($regra->aplicavelA($tipoCliente)) {
return $regra->calcular($total);
}
}
return 0.0;
}
}
// Adicionar novo tipo = criar nova classe, não tocar na calculadora
class DescontoEstudante implements RegraDeDesconto
{
public function calcular(float $total): float { return $total * 0.05; }
public function aplicavelA(string $t): bool { return $t === 'estudante'; }
}
$calculadora = new CalculadoraDeDescontoOCP([
new DescontoVip(),
new DescontoFidelidade(),
new DescontoFuncionario(),
new DescontoEstudante(), // nova regra — zero modificação na calculadora
]);
L — Liskov Substitution Principle
Subtipos devem ser substituíveis por seus tipos base sem alterar a corretude do programa. Em PHP prático: se você aceita uma interface, qualquer implementação deve se comportar conforme o contrato — sem surpresas, sem restrições adicionais.
<?php
declare(strict_types=1);
// ── PROBLEMA: subclasse viola o contrato ─────────────────────────────
class Retangulo
{
public function __construct(
protected float $largura,
protected float $altura,
) {}
public function setLargura(float $l): void { $this->largura = $l; }
public function setAltura(float $a): void { $this->altura = $a; }
public function area(): float { return $this->largura * $this->altura; }
}
class Quadrado extends Retangulo
{
// VIOLA LSP: setLargura altera AMBAS as dimensões — surpresa para o caller
public function setLargura(float $l): void { $this->largura = $this->altura = $l; }
public function setAltura(float $a): void { $this->largura = $this->altura = $a; }
}
function calcularArea(Retangulo $r): float
{
$r->setLargura(5);
$r->setAltura(4);
return $r->area();
// Para Retangulo: retorna 20 — correto
// Para Quadrado: retorna 16 — viola a expectativa do caller!
}
// ── SOLUÇÃO: modelar corretamente o domínio ───────────────────────────
interface Forma
{
public function area(): float;
public function perimetro(): float;
}
final class Retangulo2 implements Forma
{
public function __construct(
private readonly float $largura,
private readonly float $altura,
) {}
public function area(): float { return $this->largura * $this->altura; }
public function perimetro(): float { return 2 * ($this->largura + $this->altura); }
}
final class Quadrado2 implements Forma
{
public function __construct(private readonly float $lado) {}
public function area(): float { return $this->lado ** 2; }
public function perimetro(): float { return 4 * $this->lado; }
}
// Qualquer Forma pode ser substituída — sem surpresas
function imprimirInfo(Forma $f): void
{
echo "Área: {$f->area()}, Perímetro: {$f->perimetro()}\n";
}
I — Interface Segregation Principle
Clientes não devem depender de interfaces que não usam. Interfaces gordas forçam implementações vazias ou lançam UnsupportedOperationException — sinal claro de violação.
<?php
declare(strict_types=1);
// ── PROBLEMA: interface gorda ─────────────────────────────────────────
interface Trabalhador
{
public function trabalhar(): void;
public function comer(): void;
public function dormir(): void;
public function receberSalario(): void;
}
// Robô não come nem dorme — mas é forçado a implementar
class RoboTrabalhador implements Trabalhador
{
public function trabalhar(): void { echo "Trabalhando...\n"; }
public function comer(): void { throw new \LogicException('Robôs não comem!'); }
public function dormir(): void { throw new \LogicException('Robôs não dormem!'); }
public function receberSalario(): void { /* Robô não recebe */ }
}
// ── SOLUÇÃO: interfaces coesas e focadas ──────────────────────────────
interface Trabalhavel
{
public function trabalhar(): void;
}
interface Alimentavel
{
public function comer(): void;
public function dormir(): void;
}
interface Remuneravel
{
public function receberSalario(float $valor): void;
}
// Funcionário humano implementa tudo
class FuncionarioHumano implements Trabalhavel, Alimentavel, Remuneravel
{
public function trabalhar(): void { echo "Trabalhando...\n"; }
public function comer(): void { echo "Almoçando...\n"; }
public function dormir(): void { echo "Descansando...\n"; }
public function receberSalario(float $v): void { echo "Recebeu R$ {$v}\n"; }
}
// Robô só implementa o que faz sentido
class RoboTrabalhador2 implements Trabalhavel
{
public function trabalhar(): void { echo "Processando...\n"; }
}
// Código que depende apenas do que precisa
function executarTurno(Trabalhavel $trabalhador): void
{
$trabalhador->trabalhar();
// Não sabe e não precisa saber se é humano ou robô
}
D — Dependency Inversion Principle
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. A abstração não deve depender de detalhes — os detalhes devem depender da abstração.
<?php
declare(strict_types=1);
// ── PROBLEMA: acoplamento direto à implementação ──────────────────────
class ProcessadorDePedido
{
// Acoplado ao MySQL — trocar de banco = modificar esta classe
private \PDO $pdo;
public function __construct()
{
// new dentro do construtor = dependência hardcoded
$this->pdo = new \PDO('mysql:host=localhost;dbname=loja', 'root', '');
}
public function processar(int $pedidoId): void
{
$stmt = $this->pdo->prepare('UPDATE pedidos SET status = ? WHERE id = ?');
$stmt->execute(['processado', $pedidoId]);
}
}
// ── SOLUÇÃO: depender de abstrações, não de implementações ────────────
interface PedidoRepositorioInterface
{
public function atualizarStatus(int $pedidoId, string $status): void;
public function buscarPorId(int $id): ?array;
}
interface NotificadorInterface
{
public function notificar(string $destinatario, string $mensagem): void;
}
// Módulo de alto nível depende apenas de interfaces
class ProcessadorDePedidoDIP
{
public function __construct(
private readonly PedidoRepositorioInterface $repositorio,
private readonly NotificadorInterface $notificador,
) {}
public function processar(int $pedidoId): void
{
$pedido = this->repositorio->buscarPorId($pedidoId);
if ($pedido === null) {
throw new \RuntimeException("Pedido #{$pedidoId} não encontrado.");
}
$this->repositorio->atualizarStatus($pedidoId, 'processado');
$this->notificador->notificar($pedido['email_cliente'], "Pedido #{$pedidoId} processado.");
}
}
// Implementações concretas — detalhes que dependem da abstração
class PedidoRepositorioPDO implements PedidoRepositorioInterface
{
public function __construct(private readonly \PDO $pdo) {}
public function atualizarStatus(int $id, string $status): void
{
$this->pdo->prepare('UPDATE pedidos SET status = ? WHERE id = ?')
->execute([$status, $id]);
}
public function buscarPorId(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM pedidos WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
}
class NotificadorEmail implements NotificadorInterface
{
public function notificar(string $destinatario, string $mensagem): void
{
mail($destinatario, 'Atualização do pedido', $mensagem);
}
}
// Fácil de testar — basta injetar mocks
class NotificadorFake implements NotificadorInterface
{
public array $mensagensEnviadas = [];
public function notificar(string $d, string $m): void
{
$this->mensagensEnviadas[] = ['destinatario' => $d, 'mensagem' => $m];
}
}
Resumo do artigo
| Princípio | Problema resolvido | Sinal de violação |
|---|---|---|
| SRP | Classe com múltiplos motivos para mudar | Método com mais de uma responsabilidade |
| OCP | Modificar código existente para novos casos | if/elseif crescendo a cada novo tipo |
| LSP | Subclasse quebra expectativas do contrato | Método herdado lança LogicException |
| ISP | Interface forçando métodos não usados | Implementações com throw new LogicException |
| DIP | Acoplamento a implementações concretas | new dentro de construtores, classes concretas como parâmetro |
Exercício da semana
-
Refatore o
ProdutoControllerdo Artigo 23 para aplicar SRP: extraia a validação paraProdutoValidator, a leitura do corpo paraRequestReader, e a serialização paraProdutoSerializer. O controller deve apenas orquestrar. -
Aplique OCP ao sistema de notificações: crie uma interface
CanalDeNotificacaocom métodoenviar(string $destinatario, string $mensagem): voide implementeNotificacaoEmail,NotificacaoSMSeNotificacaoSlack. ONotificacaoServicedeve aceitar um array de canais. -
Identifique a violação de LSP no seguinte cenário:
ColecaoOrdenada extends ColecaoondeColecao::adicionar()aceita qualquer posição eColecaoOrdenada::adicionar()ignora a posição e mantém ordenação. Proponha uma solução com interfaces separadas. -
Refatore o
MigrationRunneraplicando DIP: crie uma interfaceLoggerInterfacecom métodolog(string $mensagem): voide injete-a no runner em vez de usarechodiretamente. ImplementeLoggerConsoleeLoggerArray(para testes). -
Desafio: escreva testes unitários que provam os princípios SOLID no seu código. Por exemplo: para DIP, o teste injeta um mock e verifica que
ProcessadorDePedidonão instancia nada diretamente. Para OCP, adicione uma novaRegraDeDescontoe verifique que os testes existentes continuam passando.
Referências
- Uncle Bob — The SOLID Principles: https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
- PHP The Right Way — Design Patterns: https://phptherightway.com/#design_patterns
- Refactoring Guru — SOLID: https://refactoring.guru/pt-br/design-patterns
- PHP Manual — Interfaces: https://www.php.net/manual/pt_BR/language.oop5.interfaces.php