Bem-vindo à aula 58 do nosso curso de PHP! Hoje vamos mergulhar em um tópico que separa desenvolvedores que apenas escrevem código funcional daqueles que constroem software sustentável e de alta qualidade: Clean Code. O termo foi popularizado por Robert C. Martin (Uncle Bob) no livro homônimo, e se refere a um conjunto de práticas que visam tornar o código legível, simples e fácil de manter. Em projetos reais, passamos muito mais tempo lendo código do que escrevendo, então a clareza é fundamental.

Nesta aula, vamos explorar os pilares do clean code no contexto do PHP: como escolher bons nomes, como manter funções pequenas e coesas, uma introdução aos princípios SOLID (que são a base do design orientado a objetos) e como identificar os famosos "code smells" (maus cheiros no código) que indicam problemas de design. Ao final, você terá um guia prático para elevar a qualidade do seu código PHP.

Nomes

Nomes significativos são a base do clean code. Um bom nome elimina a necessidade de comentários explicativos e torna o código autodocumentável. No PHP, assim como em outras linguagens, devemos escolher nomes que revelem a intenção do que a variável, função ou classe representa. Evite abreviações confusas, siglas obscuras e nomes genéricos como $dados ou $x.

Além de revelar a intenção, os nomes devem ser pronunciáveis e buscáveis. Por exemplo, $cpfDoCliente é melhor que $c. Devemos também seguir as convenções da comunidade PHP: classes em PascalCase, métodos e funções em camelCase, e variáveis em camelCase ou snake_case (o PSR-1 recomenda camelCase para métodos e snake_case para variáveis, mas o importante é ser consistente).

// Ruim
$a = 30;
$b = 0.1;
$c = $a * $b;

// Bom
$valorTotal = 30;
$taxaJuros = 0.1;
$valorComJuros = $valorTotal * $taxaJuros;

// Ruim
function calc($v) {
    return $v * 1.1;
}

// Bom
function calcularValorComImposto(float $valor): float {
    return $valor * 1.1;
}

Os nomes de funções devem começar com um verbo, como obter, calcular, validar. Isso comunica claramente a ação executada. Para booleanos, use prefixos como is, has ou can, por exemplo, isAtivo(), hasPermissao(). Essas pequenas escolhas fazem uma enorme diferença na leitura do código.

Funções pequenas

Funções pequenas são mais fáceis de entender, testar e reutilizar. A regra prática é que uma função deve fazer apenas uma coisa e fazê-la bem. Se você tem que explicar o que a função faz em mais de uma frase, provavelmente ela está fazendo demais. O ideal é que uma função tenha entre 5 e 15 linhas, mas o mais importante é a coesão: todas as linhas devem contribuir para um único objetivo.

Para manter funções pequenas, extraia blocos de código que representam uma ideia distinta em funções separadas. Isso também melhora a legibilidade, pois o nome da função extraída documenta o que aquele trecho faz. Veja um exemplo de função grande que faz tudo:

function processarPedido(Pedido $pedido, array $itens): void {
    $total = 0;
    foreach ($itens as $item) {
        $total += $item['preco'] * $item['quantidade'];
    }
    $pedido->setTotal($total);

    // Aplicar desconto
    if ($total > 100) {
        $pedido->setTotal($total * 0.9);
    }

    // Enviar e-mail de confirmação
    $mensagem = "Pedido #" . $pedido->getId() . " confirmado. Total: R$ " . $pedido->getTotal();
    mail($pedido->getCliente()->getEmail(), "Confirmação de Pedido", $mensagem);
}

Essa função faz pelo menos três coisas: calcula o total, aplica desconto e envia e-mail. Vamos refatorá-la em funções pequenas e coesas:

function processarPedido(Pedido $pedido, array $itens): void {
    $total = calcularTotal($itens);
    $totalComDesconto = aplicarDesconto($total);
    $pedido->setTotal($totalComDesconto);
    enviarEmailConfirmacao($pedido);
}

function calcularTotal(array $itens): float {
    $total = 0;
    foreach ($itens as $item) {
        $total += $item['preco'] * $item['quantidade'];
    }
    return $total;
}

function aplicarDesconto(float $total): float {
    if ($total > 100) {
        return $total * 0.9;
    }
    return $total;
}

function enviarEmailConfirmacao(Pedido $pedido): void {
    $mensagem = "Pedido #" . $pedido->getId() . " confirmado. Total: R$ " . $pedido->getTotal();
    mail($pedido->getCliente()->getEmail(), "Confirmação de Pedido", $mensagem);
}

Agora cada função tem uma responsabilidade única, e a função principal lê como uma narrativa. Isso também facilita a criação de testes unitários para cada parte.

SOLID (introdução)

SOLID é um acrônimo para cinco princípios de design orientado a objetos que ajudam a criar sistemas mais manuteníveis e escaláveis. Eles foram propostos por Robert C. Martin e são amplamente adotados na comunidade PHP, especialmente em frameworks como Laravel e Symfony. Nesta introdução, vamos explorar cada princípio brevemente, com exemplos práticos em PHP.

Os cinco princípios são: S — Single Responsibility Principle (Princípio da Responsabilidade Única); O — Open/Closed Principle (Princípio Aberto/Fechado); L — Liskov Substitution Principle (Princípio da Substituição de Liskov); I — Interface Segregation Principle (Princípio da Segregação de Interfaces); e D — Dependency Inversion Principle (Princípio da Inversão de Dependência). Vamos detalhar cada um.

Single Responsibility Principle (SRP)

Uma classe deve ter apenas um motivo para mudar, ou seja, deve ter uma única responsabilidade. Isso não significa que ela deve ter apenas um método, mas que todas as suas funcionalidades devem estar relacionadas a um propósito específico. Por exemplo, uma classe que lida com persistência de dados e também com envio de e-mails viola o SRP.

// Ruim: Classe faz duas coisas
class UsuarioService {
    public function salvarUsuario(array $dados): void {
        // salva no banco
    }
    public function enviarEmailBoasVindas(string $email): void {
        // envia e-mail
    }
}

// Bom: Classes separadas
class UsuarioRepository {
    public function salvar(array $dados): void {
        // salva no banco
    }
}

class EmailService {
    public function enviarBoasVindas(string $email): void {
        // envia e-mail
    }
}

Open/Closed Principle (OCP)

Classes devem estar abertas para extensão, mas fechadas para modificação. Ou seja, você deve poder adicionar novos comportamentos sem alterar o código existente. Isso geralmente é alcançado com herança, interfaces ou composição. Um exemplo clássico é uma calculadora de desconto que pode ser estendida com novos tipos de desconto sem modificar a classe original.

interface Desconto {
    public function calcular(float $total): float;
}

class DescontoFixo implements Desconto {
    public function __construct(private float $valor) {}
    public function calcular(float $total): float {
        return $total - $this->valor;
    }
}

class DescontoPercentual implements Desconto {
    public function __construct(private float $percentual) {}
    public function calcular(float $total): float {
        return $total * (1 - $this->percentual / 100);
    }
}

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

Liskov Substitution Principle (LSP)

Subclasses devem ser substituíveis por suas classes base sem alterar o comportamento correto do programa. Em outras palavras, se você tem uma classe Pato e uma subclasse PatoDeBorracha, o programa deve funcionar corretamente se você usar qualquer uma delas onde um Pato é esperado (desde que o comportamento esperado seja o mesmo). Isso implica que as subclasses não devem violar as invariantes da classe base.

class Retangulo {
    public function __construct(protected float $largura, protected float $altura) {}
    public function setLargura(float $largura): void { $this->largura = $largura; }
    public function setAltura(float $altura): void { $this->altura = $altura; }
    public function area(): float { return $this->largura * $this->altura; }
}

class Quadrado extends Retangulo {
    public function setLargura(float $largura): void {
        $this->largura = $largura;
        $this->altura = $largura;
    }
    public function setAltura(float $altura): void {
        $this->altura = $altura;
        $this->largura = $altura;
    }
}

// Violação: se esperarmos um Retangulo, um Quadrado pode quebrar a lógica
function calcularArea(Retangulo $r): float {
    $r->setLargura(5);
    $r->setAltura(10);
    return $r->area(); // Esperado 50, mas com Quadrado retorna 100
}

Nesse exemplo, a classe Quadrado viola o LSP porque altera o comportamento esperado. Uma solução seria não herdar de Retangulo e usar composição ou uma interface comum.

Interface Segregation Principle (ISP)

Clientes não devem ser forçados a depender de interfaces que não usam. É melhor ter várias interfaces específicas do que uma única interface geral. Por exemplo, uma interface Animal com métodos voar(), nadar() e correr() seria ruim para um Cachorro que não voa. Em vez disso, crie interfaces menores.

// Ruim
interface Animal {
    public function voar();
    public function nadar();
    public function correr();
}

class Cachorro implements Animal {
    public function voar() { /* ??? */ }
    public function nadar() { /* ok */ }
    public function correr() { /* ok */ }
}

// Bom
interface Corredor { public function correr(); }
interface Nadador { public function nadar(); }
interface Voador { public function voar(); }

class Cachorro implements Corredor, Nadador {
    public function correr() { /* ok */ }
    public function nadar() { /* ok */ }
}

Dependency Inversion Principle (DIP)

Módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações. Além disso, abstrações não devem depender de detalhes; detalhes devem depender de abstrações. Isso é o princípio da inversão de dependência, que promove o uso de interfaces para desacoplar o código. Na prática, isso é feito com injeção de dependência.

interface Logger {
    public function log(string $mensagem): void;
}

class ArquivoLogger implements Logger {
    public function log(string $mensagem): void {
        file_put_contents('log.txt', $mensagem, FILE_APPEND);
    }
}

class PedidoService {
    public function __construct(private Logger $logger) {}
    public function criarPedido(): void {
        // lógica
        $this->logger->log('Pedido criado');
    }
}

// Uso
$service = new PedidoService(new ArquivoLogger());

Code smells

Code smells são sinais no código que indicam possíveis problemas de design ou manutenibilidade. Eles não são bugs, mas são indícios de que o código pode precisar de refatoração. Identificar esses cheiros é o primeiro passo para melhorar a qualidade do código. Vamos listar os mais comuns no mundo PHP.

  • Métodos longos: Funções com muitas linhas, que tentam fazer várias coisas, como vimos na seção anterior.
  • Classes grandes: Classes com muitas responsabilidades e muitos métodos, frequentemente chamadas de "God Objects".
  • Duplicação de código: Trechos de código repetidos em vários lugares. Isso dificulta a manutenção, pois uma correção precisa ser feita em todos os pontos.
  • Listas longas de parâmetros: Funções que recebem muitos parâmetros (mais de 3 ou 4) são difíceis de usar e entender. Considere agrupar parâmetros relacionados em um objeto (Parameter Object).
  • Comentários desnecessários: Comentários que explicam o que o código faz, em vez de explicar o porquê. Se o código precisa de comentários para ser entendido, provavelmente os nomes não são bons.
  • Código morto: Trechos de código que nunca são executados, como variáveis não utilizadas ou funções não chamadas.
// Exemplo de mau cheiro: lista longa de parâmetros
function criarUsuario(string $nome, string $email, string $telefone, string $endereco, string $cidade, string $estado, string $cep): void {
    // ...
}

// Refatoração: usar um objeto DTO
class UsuarioDTO {
    public function __construct(
        public string $nome,
        public string $email,
        public string $telefone,
        public Endereco $endereco
    ) {}
}

function criarUsuario(UsuarioDTO $dto): void {
    // ...
}

Outro smell comum é o uso de switch ou if-else grandes que podem ser substituídos por polimorfismo. Sempre que você vir um switch que verifica o tipo de uma variável, considere se uma solução orientada a objetos seria mais limpa.

Boas práticas e observações finais

Além dos tópicos acima, aqui vão algumas boas práticas que complementam o clean code em PHP:

  • Use type hints: Declare tipos de parâmetros e retornos para tornar o código mais previsível e auto-documentado.
  • Evite efeitos colaterais: Funções devem idealmente ser puras, ou seja, não modificar variáveis globais ou estado externo sem necessidade.
  • Use constantes: Em vez de números mágicos ou strings espalhadas, defina constantes com nomes significativos.
  • Prefira composição à herança: A herança pode criar acoplamento forte; a composição é mais flexível.
  • Refatore continuamente: Clean code não é um destino, mas um processo. Sempre que tocar em um código, deixe-o um pouco melhor do que encontrou.

Referências

Exercícios

  1. Refatore o seguinte código para usar nomes significativos e funções pequenas:
    function d($a, $b) {
        $c = $a * $b;
        if ($c > 100) {
            $c = $c * 0.9;
        }
        return $c;
    }
    

    ✓ Resposta:
    function calcularValorComDesconto(float $valor, float $multiplicador): float {
        $total = $valor * $multiplicador;
        if ($total > 100) {
            return aplicarDesconto($total);
        }
        return $total;
    }
    
    function aplicarDesconto(float $total): float {
        return $total * 0.9;
    }
    
  2. Identifique e corrija a violação do Single Responsibility Principle na classe abaixo:
    class Relatorio {
        public function gerar(): string {
            // gera o relatório
            return "relatorio";
        }
        public function salvarEmArquivo(string $conteudo): void {
            file_put_contents('relatorio.txt', $conteudo);
        }
    }
    

    ✓ Resposta: Separar em duas classes: Relatorio que gera o conteúdo e ArquivoService que salva em arquivo.
  3. Explique o princípio Open/Closed e dê um exemplo em PHP.

    ✓ Resposta: O princípio Open/Closed (OCP) afirma que classes devem estar abertas para extensão, mas fechadas para modificação. Exemplo: usar interfaces para adicionar novos tipos de desconto sem alterar a classe que os utiliza.
  4. Qual é o code smell presente no código abaixo? Como você corrigiria?
    function processar($tipo) {
        if ($tipo == 'email') {
            // envia email
        } elseif ($tipo == 'sms') {
            // envia sms
        } elseif ($tipo == 'push') {
            // envia push
        }
    }
    

    ✓ Resposta: O code smell é o uso de condicionais para tipos (switch statement). Corrigir usando polimorfismo: criar uma interface Notificador com implementações para cada tipo.
  5. Escreva uma função PHP que siga o princípio da responsabilidade única e receba um array de números e retorne a soma dos pares. Use nomes adequados.

    ✓ Resposta:
    function somarNumerosPares(array $numeros): int {
        $soma = 0;
        foreach ($numeros as $numero) {
            if ($numero % 2 === 0) {
                $soma += $numero;
            }
        }
        return $soma;
    }