PHP

Design Patterns: Singleton, Factory e Builder Já leu

15 min de leitura

Design Patterns: Singleton, Factory e Builder
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 categori

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
Problema Múltiplas instâncias de um recurso compartilhado
Solução Construtor privado + instância estática
Uso real Conexão de banco, Logger, Config
Factory Method
Problema Código cliente acopla-se a classes concretas
Solução Método que cria objetos sem expor a classe
Uso real Drivers de BD, Notificações, Parsers
Builder
Problema Construtores com muitos parâmetros opcionais
Solução Objeto dedicado que monta o produto passo a passo
Uso real QueryBuilder, Email, Config, HTTP Client

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.

 
src/Database/Conexao.phpPHP
<?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");
⚠️ Singleton — use com moderação

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.

❌ Sem Factory — acoplamento direto

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.

✅ Com Factory — desacoplado

O cliente pede uma notificação para a factory e recebe um objeto que implementa a interface. Adicionar WhatsApp não altera o cliente.

 
src/Notificacoes/NotificacaoFactory.phpPHP
<?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:

 
abstract-factory.phpPHP
<?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.

 
src/Mail/EmailBuilder.phpPHP
<?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:

 
director.phpPHP
<?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%");
✅ Builder no mundo real — QueryBuilder do Laravel / Doctrine

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

  1. Implemente um Singleton para gerenciar configurações do sistema (Config). Ele deve carregar um arquivo config.php que retorna um array na primeira chamada e expor métodos get(string $chave, mixed $padrao = null): mixed e set(string $chave, mixed $valor): void. Verifique com === que duas chamadas a Config::getInstance() retornam o mesmo objeto.
  2. Crie uma Factory para parsers de arquivo: interface ParserInterface com método parse(string $conteudo): array. Implemente JsonParser, CsvParser e XmlParser. A ParserFactory::criar(string $extensao) retorna o parser correto baseado na extensão (.json, .csv, .xml). Lança ParserNaoSuportadoException para extensões desconhecidas.
  3. Construa um Builder para gerar queries SQL SELECT: QueryBuilder com métodos from(string $tabela), select(string ...$colunas), where(string $condicao), orderBy(string $coluna, string $direcao = 'ASC'), limit(int $n) e build(): string. O build() retorna a string SQL completa e válida.
  4. Crie um Director para o QueryBuilder do exercício 3 com dois métodos: queryPaginada(string $tabela, int $pagina, int $porPagina): string e queryRecentes(string $tabela, string $campoData, int $limite): string.
  5. Desafio: combine os três padrões num mini-sistema de relatórios. A RelatorioFactory cria implementações de RelatorioInterface (PDF, CSV, HTML). Cada relatório é configurado por um RelatorioBuilder com filtros de data, colunas visíveis e ordenação. O ConfiguracaoRelatorio é um Singleton que guarda os formatos disponíveis e os limites de linhas por relatório.

Referências e leituras para aprofundar

Comentários

Mais em PHP

Funções
Funções

At&eacute; agora escrevemos c&oacute;digo sequencial &mdash; uma instru&ccedi...

Arrays em Profundidade
Arrays em Profundidade

Arrays s&atilde;o a estrutura de dados mais usada no PHP. Praticamente tudo q...

Cache Avançado com Redis
Cache Avançado com Redis

Cache é a diferença entre uma query de 200ms que roda mil vezes por minuto e...