PHP

SOLID na Prática Já leu

10 min de leitura

SOLID na Prática
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

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

  1. Refatore o ProdutoController do Artigo 23 para aplicar SRP: extraia a validação para ProdutoValidator, a leitura do corpo para RequestReader, e a serialização para ProdutoSerializer. O controller deve apenas orquestrar.

  2. Aplique OCP ao sistema de notificações: crie uma interface CanalDeNotificacao com método enviar(string $destinatario, string $mensagem): void e implemente NotificacaoEmail, NotificacaoSMS e NotificacaoSlack. O NotificacaoService deve aceitar um array de canais.

  3. Identifique a violação de LSP no seguinte cenário: ColecaoOrdenada extends Colecao onde Colecao::adicionar() aceita qualquer posição e ColecaoOrdenada::adicionar() ignora a posição e mantém ordenação. Proponha uma solução com interfaces separadas.

  4. Refatore o MigrationRunner aplicando DIP: crie uma interface LoggerInterface com método log(string $mensagem): void e injete-a no runner em vez de usar echo diretamente. Implemente LoggerConsole e LoggerArray (para testes).

  5. 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 ProcessadorDePedido não instancia nada diretamente. Para OCP, adicione uma nova RegraDeDesconto e 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
Comentários

Mais em PHP

Configurando o Ambiente de Desenvolvimento
Configurando o Ambiente de Desenvolvimento

Antes de escrever qualquer programa, voc&ecirc; precisa de um ambiente onde o...

Autenticação JWT
Autenticação JWT

Autenticação é o processo de verificar quem é o cliente. Em APIs REST statele...

Upload de Arquivos
Upload de Arquivos

Upload de arquivos parece simples — e na superfície é. Mas quando você consid...