Design Patterns (Padrões de Projeto) são soluções codificadas para problemas que aparecem repetidamente no design de software orientado a objetos. O livro Design Patterns (GoF, 1994) catalogou 23 padrões em três categorias: criacionais (como criar objetos), estruturais (como compor objetos) e comportamentais (como objetos comunicam).
Neste artigo cobrimos os três padrões criacionais mais usados em PHP: Singleton (garante uma única instância), Factory Method (delega a criação a subclasses) e Builder (constrói objetos complexos passo a passo). Você os encontrará diretamente em Laravel, Symfony e Doctrine.
Os três padrões — visão geral
Singleton — uma única instância
O Singleton garante que uma classe tenha exatamente uma instância durante toda a execução, e fornece um ponto de acesso global a ela. O caso de uso mais clássico é a conexão de banco de dados — abrir uma nova conexão a cada chamada seria caro e desnecessário. O padrão usa três elementos: construtor privado (impede new externo), propriedade estática para guardar a instância, e método estático getInstance() que cria ou retorna a instância existente.
<?php declare(strict_types=1); namespace MeuApp\Database; use PDO; use PDOException; final class Conexao { // A instância única fica guardada nesta propriedade estática private static ?self $instancia = null; private PDO $pdo; // Construtor privado — nenhum código externo pode chamar "new Conexao()" private function __construct( string $dsn, string $usuario, string $senha, ) { try { $this->pdo = new PDO($dsn, $usuario, $senha, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (PDOException $e) { throw new \RuntimeException("Falha ao conectar ao banco: " . $e->getMessage()); } } // Método estático — único ponto de acesso à instância public static function getInstance( string $dsn = "mysql:host=localhost;dbname=app;charset=utf8mb4", string $usuario = "root", string $senha = "", ): static { // Cria a instância apenas na primeira chamada if (self::$instancia === null) { self::$instancia = new self($dsn, $usuario, $senha); } return self::$instancia; } // Impede clonagem — mantém a unicidade private function __clone(): void {} public function pdo(): PDO { return $this->pdo; } public function buscarTodos(string $tabela): array { return $this->pdo ->query("SELECT * FROM {$tabela}") ->fetchAll(); } } // Uso — todas as chamadas retornam a MESMA instância $db1 = Conexao::getInstance(); $db2 = Conexao::getInstance(); var_dump($db1 === $db2); // bool(true) — mesma instância na memória $produtos = Conexao::getInstance()->buscarTodos("produtos");
O Singleton é frequentemente apontado como um antipadrão porque cria estado global, dificulta testes unitários (não dá para substituir por um mock facilmente) e introduz acoplamento oculto. Em PHP moderno, a prática preferida é injeção de dependência — um container DI (como o do Laravel ou Symfony) gerencia a criação e o ciclo de vida dos objetos. Use Singleton com consciência: é aceitável para conexões de banco, loggers e configurações; evite para lógica de negócio.
Factory Method — delegar a criação
O Factory Method define um método para criar objetos, mas deixa as subclasses (ou uma classe factory dedicada) decidirem qual classe concreta instanciar. O cliente passa a depender de uma interface ou classe abstrata, não de implementações específicas. Isso elimina condicionais if/switch espalhados pelo código toda vez que você precisa criar um objeto diferente com base em um parâmetro.
O código cliente precisa conhecer todas as classes concretas e decidir qual instanciar. Cada novo tipo de notificação exige alterar este código.
O cliente pede uma notificação para a factory e recebe um objeto que implementa a interface. Adicionar WhatsApp não altera o cliente.
<?php declare(strict_types=1); namespace MeuApp\Notificacoes; use InvalidArgumentException; // Interface — o contrato que toda notificação deve cumprir interface NotificacaoInterface { public function enviar(string $destinatario, string $mensagem): bool; public function canal(): string; } // Implementações concretas class NotificacaoEmail implements NotificacaoInterface { public function enviar(string $dest, string $msg): bool { // Em produção: chamar um provedor como Mailgun, SendGrid, SES echo "📧 Email → {$dest}: {$msg}\n"; return true; } public function canal(): string { return "email"; } } class NotificacaoSMS implements NotificacaoInterface { public function enviar(string $dest, string $msg): bool { // Em produção: chamar Twilio, AWS SNS, Vonage echo "📱 SMS → {$dest}: {$msg}\n"; return true; } public function canal(): string { return "sms"; } } class NotificacaoSlack implements NotificacaoInterface { public function enviar(string $dest, string $msg): bool { echo "💬 Slack → #{$dest}: {$msg}\n"; return true; } public function canal(): string { return "slack"; } } // Factory — cria a implementação correta com base em uma string // O cliente não precisa conhecer nenhuma classe concreta final class NotificacaoFactory { // Mapa de tipo → classe — para adicionar WhatsApp: uma linha aqui private const TIPOS = [ "email" => NotificacaoEmail::class, "sms" => NotificacaoSMS::class, "slack" => NotificacaoSlack::class, ]; public static function criar(string $tipo): NotificacaoInterface { if (!isset(self::TIPOS[$tipo])) { throw new InvalidArgumentException( "Canal desconhecido: '{$tipo}'. Disponíveis: " . implode(", ", array_keys(self::TIPOS)) ); } $classe = self::TIPOS[$tipo]; return new $classe(); } // Variante — cria múltiplos canais de uma vez public static function criarMultiplos(string ...$tipos): array { return array_map(self::criar(...), $tipos); } } // Uso — o cliente só conhece a interface e a factory $canais = NotificacaoFactory::criarMultiplos("email", "sms", "slack"); foreach ($canais as $canal) { $canal->enviar("ana@email.com", "Pedido confirmado!"); } // 📧 Email → ana@email.com: Pedido confirmado! // 📱 SMS → ana@email.com: Pedido confirmado! // 💬 Slack → #ana@email.com: Pedido confirmado! // Lendo canal preferido de uma config, banco de dados, etc. $canalPreferido = "sms"; // viria de $_ENV["NOTIFICACAO_CANAL"] $n = NotificacaoFactory::criar($canalPreferido); echo $n->canal(); // sms
Abstract Factory — fábrica de famílias
Quando você precisa criar famílias inteiras de objetos relacionados — por exemplo, um conjunto consistente de componentes de UI para diferentes temas — o Abstract Factory estende o conceito. Cada factory concreta produz objetos compatíveis entre si:
<?php declare(strict_types=1); // Abstract Factory — cria famílias de objetos relacionados // Exemplo: diferentes drivers de armazenamento com API consistente interface StorageFactory { public function criarLeitor(): Leitor; public function criarEscritor(): Escritor; } interface Leitor { public function ler(string $chave): mixed; } interface Escritor { public function escrever(string $chave, mixed $valor): void; } // Família Redis class RedisLeitor implements Leitor { public function ler(string $k): mixed { return "[Redis:get {$k}]"; } } class RedisEscritor implements Escritor { public function escrever(string $k, mixed $v): void { echo "[Redis:set {$k}={$v}]\n"; } } // Família Arquivo class ArquivoLeitor implements Leitor { public function ler(string $k): mixed { return "[File:read {$k}]"; } } class ArquivoEscritor implements Escritor { public function escrever(string $k, mixed $v): void { echo "[File:write {$k}={$v}]\n"; } } class RedisFactory implements StorageFactory { public function criarLeitor(): Leitor { return new RedisLeitor(); } public function criarEscritor(): Escritor { return new RedisEscritor(); } } class ArquivoFactory implements StorageFactory { public function criarLeitor(): Leitor { return new ArquivoLeitor(); } public function criarEscritor(): Escritor { return new ArquivoEscritor(); } } // Código cliente usa só a interface — pode trocar Redis por Arquivo // sem modificar uma linha desta função function executarCache(StorageFactory $factory): void { $escritor = $factory->criarEscritor(); $leitor = $factory->criarLeitor(); $escritor->escrever("sessao", "abc123"); echo $leitor->ler("sessao") . "\n"; } executarCache(new RedisFactory()); // [Redis:set sessao=abc123] // [Redis:get sessao] executarCache(new ArquivoFactory()); // [File:write sessao=abc123] // [File:read sessao]
Builder — construindo objetos complexos
O Builder separa a construção de um objeto complexo da sua representação final. É a solução elegante para o problema do "construtor telescópico" — quando uma classe precisa de muitos parâmetros opcionais, criar um construtor com 10 argumentos é impraticável e propenso a erros (fácil trocar a ordem dos argumentos). O Builder oferece uma API fluente onde cada parâmetro opcional é um método com nome descritivo.
<?php declare(strict_types=1); namespace MeuApp\Mail; // O produto — objeto final imutável que o Builder constrói final class Email { public function __construct( public readonly string $de, public readonly string $para, public readonly string $assunto, public readonly string $corpo, public readonly array $cc = [], public readonly array $bcc = [], public readonly array $anexos = [], public readonly bool $html = false, public readonly ?string $replyTo = null, public readonly int $prioridade = 3, ) {} } // Builder — constrói o Email passo a passo com API fluente final class EmailBuilder { private string $de; private string $para; private string $assunto; private string $corpo; private array $cc = []; private array $bcc = []; private array $anexos = []; private bool $html = false; private ?string $replyTo = null; private int $prioridade = 3; // Construtor factory estático — ponto de entrada semântico public static function novo(string $de, string $para): static { $b = new static(); $b->de = $de; $b->para = $para; return $b; } // Cada método retorna $this — permite encadeamento fluente public function assunto(string $assunto): static { $this->assunto = $assunto; return $this; } public function corpo(string $corpo, bool $html = false): static { $this->corpo = $corpo; $this->html = $html; return $this; } public function cc(string ...$enderecos): static { $this->cc = array_merge($this->cc, $enderecos); return $this; } public function bcc(string ...$enderecos): static { $this->bcc = array_merge($this->bcc, $enderecos); return $this; } public function anexar(string $caminhoArquivo): static { $this->anexos[] = $caminhoArquivo; return $this; } public function replyTo(string $endereco): static { $this->replyTo = $endereco; return $this; } public function urgente(): static { $this->prioridade = 1; // 1 = alta, 3 = normal, 5 = baixa return $this; } // build() — valida e cria o objeto final imutável public function build(): Email { if (empty($this->assunto)) { throw new \InvalidArgumentException("Email precisa de assunto."); } if (empty($this->corpo)) { throw new \InvalidArgumentException("Email precisa de corpo."); } return new Email( $this->de, $this->para, $this->assunto, $this->corpo, $this->cc, $this->bcc, $this->anexos, $this->html, $this->replyTo, $this->prioridade, ); } } // Uso — API fluente legível como uma frase $email = EmailBuilder::novo("sistema@app.com", "cliente@email.com") ->assunto("Confirmação do pedido #1234") ->corpo("<h1>Pedido confirmado!</h1><p>Obrigado pela compra.</p>", true) ->cc("gerente@app.com") ->bcc("auditoria@app.com") ->anexar("/tmp/nota-fiscal-1234.pdf") ->replyTo("suporte@app.com") ->build(); echo $email->assunto; // Confirmação do pedido #1234 var_dump($email->html); // bool(true) var_dump($email->anexos); // ["/tmp/nota-fiscal-1234.pdf"] // Email urgente — só adiciona o que precisa $alerta = EmailBuilder::novo("sistema@app.com", "ops@app.com") ->assunto("🚨 Servidor fora do ar") ->corpo("Disco cheio em prod-01. Ação imediata necessária.") ->urgente() ->build(); var_dump($alerta->prioridade); // int(1)
Builder com Director
Quando certas configurações de Build se repetem muito — como um "email de boas-vindas" ou uma "query de relatório mensal" — um Director encapsula a sequência de chamadas ao Builder, criando um ponto reutilizável para construções pré-definidas:
<?php declare(strict_types=1); use MeuApp\Mail\EmailBuilder; use MeuApp\Mail\Email; // Director — conhece as "receitas" de construção mais comuns final class EmailDirector { public static function boasVindas(string $para, string $nome): Email { return EmailBuilder::novo("noreply@app.com", $para) ->assunto("Bem-vindo ao App, {$nome}!") ->corpo("<h1>Olá, {$nome}!</h1><p>Sua conta foi criada com sucesso.</p>", true) ->replyTo("suporte@app.com") ->build(); } public static function alertaCritico(string $para, string $mensagem): Email { return EmailBuilder::novo("sistema@app.com", $para) ->assunto("🚨 ALERTA CRÍTICO — " . date("d/m H:i")) ->corpo($mensagem) ->bcc("diretoria@app.com") ->urgente() ->build(); } } // Uso — limpo, intencional, sem repetição $emailBv = EmailDirector::boasVindas("joao@email.com", "João"); $emailAlerta = EmailDirector::alertaCritico("ops@app.com", "Memória RAM em 95%");
O QueryBuilder do Laravel (DB::table('users')->where('ativo', true)->orderBy('nome')->get()) é exatamente o padrão Builder. Cada where(), orderBy(), limit() acumula a configuração, e o get() (equivalente ao build()) compila tudo em SQL e executa. O mesmo vale para o QueryBuilder do Doctrine ORM e para clientes HTTP como Guzzle.
Resumo do artigo
| Padrão | Problema resolvido | Mecanismo central | Cuidados |
|---|---|---|---|
| Singleton | Múltiplas instâncias desnecessárias de um recurso caro | Construtor privado + propriedade estática | Dificulta testes; prefira injeção de dependência |
| Factory Method | Código cliente acopla-se a classes concretas | Método que retorna uma interface — oculta o new |
Não abusa com factories para objetos simples |
| Abstract Factory | Necessidade de famílias de objetos compatíveis entre si | Interface de factory que cria conjuntos relacionados | Adicionar novo produto à família exige alterar todas as factories |
| Builder | Construtor com muitos parâmetros opcionais | API fluente que acumula config + build() valida e cria |
Overhead para objetos simples; exige Director para receitas comuns |
🏠 Exercício da semana
- Implemente um Singleton para gerenciar configurações do sistema (
Config). Ele deve carregar um arquivoconfig.phpque retorna um array na primeira chamada e expor métodosget(string $chave, mixed $padrao = null): mixedeset(string $chave, mixed $valor): void. Verifique com===que duas chamadas aConfig::getInstance()retornam o mesmo objeto. - Crie uma Factory para parsers de arquivo: interface
ParserInterfacecom métodoparse(string $conteudo): array. ImplementeJsonParser,CsvParsereXmlParser. AParserFactory::criar(string $extensao)retorna o parser correto baseado na extensão (.json,.csv,.xml). LançaParserNaoSuportadoExceptionpara extensões desconhecidas. - Construa um Builder para gerar queries SQL SELECT:
QueryBuildercom métodosfrom(string $tabela),select(string ...$colunas),where(string $condicao),orderBy(string $coluna, string $direcao = 'ASC'),limit(int $n)ebuild(): string. Obuild()retorna a string SQL completa e válida. - Crie um Director para o
QueryBuilderdo exercício 3 com dois métodos:queryPaginada(string $tabela, int $pagina, int $porPagina): stringequeryRecentes(string $tabela, string $campoData, int $limite): string. - Desafio: combine os três padrões num mini-sistema de relatórios. A
RelatorioFactorycria implementações deRelatorioInterface(PDF, CSV, HTML). Cada relatório é configurado por umRelatorioBuildercom filtros de data, colunas visíveis e ordenação. OConfiguracaoRelatorioé um Singleton que guarda os formatos disponíveis e os limites de linhas por relatório.
Referências e leituras para aprofundar
-
Padrões Criacionais — Refactoring Guru Padrão https://refactoring.guru/pt-br/design-patterns/creational-patterns
Introdução em português aos cinco padrões criacionais (Singleton, Factory Method, Abstract Factory, Builder e Prototype) com diagramas e exemplos interativos.
-
Singleton — Refactoring Guru Padrão https://refactoring.guru/pt-br/design-patterns/singleton
Explicação detalhada com diagrama de classes, implementação em PHP, quando usar, e críticas ao padrão — incluindo por que é considerado um antipadrão em muitos contextos.
-
Builder — Refactoring Guru Padrão https://refactoring.guru/pt-br/design-patterns/builder
Visão completa do padrão Builder: produto, builder concreto, director, e como a API fluente é uma variação moderna do padrão original.
-
GAMMA et al. — Design Patterns (GoF). Addison-Wesley, 1994 Livro https://www.oreilly.com/library/view/design-patterns-elements/0201633612/
A fonte original. Os capítulos de Abstract Factory, Builder e Singleton são referência obrigatória para entender as intenções originais de cada padrão.
-
Design Patterns PHP — Documentação em português Guia https://designpatternsphp.readthedocs.io/pt_BR/latest/
Todos os 23 padrões GoF implementados em PHP moderno, com código pronto para rodar e testes PHPUnit. Ótimo repositório de referência para os próximos artigos.
-
MARTIN, R.C. — Clean Code. Cap. 3: Funções e construtores limpos. 2008 Livro https://www.oreilly.com/library/view/clean-code-a/9780136083238/
O argumento contra construtores com muitos parâmetros — a base teórica para o padrão Builder. Martin defende que funções/construtores ideais têm zero a três parâmetros.