No artigo anterior estudamos três padrões criacionais — como construir objetos com controle e intenção. Agora avançamos para padrões que definem como objetos se comunicam e como composição substitui herança: o Observer (comportamental), o Decorator (estrutural) e o Strategy (comportamental).
Estes três padrões formam a espinha dorsal de frameworks PHP modernos. O sistema de eventos do Laravel é Observer puro. Os middlewares de Symfony e Laravel são Decorators encadeados. Qualquer algoritmo intercambiável — autenticação, cálculo de frete, exportação de relatório — é Strategy. Entendê-los é entender como os frameworks funcionam por dentro.
Observer — o padrão de eventos
O Observer define uma relação de um-para-muitos entre objetos: quando um objeto (Subject ou Publisher) muda de estado, todos os seus dependentes (Observers ou Listeners) são notificados automaticamente. O Subject não precisa saber quem está ouvindo — apenas mantém uma lista de interessados e os notifica quando algo relevante acontece.
O problema que o Observer resolve é o acoplamento em cascata. Sem ele, quando um pedido é criado, o código de criação do pedido precisaria chamar diretamente o serviço de email, o serviço de estoque, o serviço de analytics e qualquer outro interessado. Adicionar um novo interessado exigiria modificar o código de criação. Com Observer, o criador apenas dispara um evento — e quem quiser reagir se registra de forma independente.
<?php
declare(strict_types=1);
namespace MeuApp\Eventos;
// O evento — objeto imutável que transporta os dados da ocorrência
final class PedidoCriado
{
public function __construct(
public readonly int $pedidoId,
public readonly string $clienteEmail,
public readonly float $total,
public readonly \DateTimeImmutable $ocorridoEm = new \DateTimeImmutable(),
) {}
}
// Interface do listener — toda classe que reage a eventos implementa esta
interface ListenerInterface
{
public function handle(object $evento): void;
}
// Listeners concretos — cada um reage de forma completamente independente
class EnviarEmailConfirmacao implements ListenerInterface
{
public function handle(object $evento): void
{
// Verifica se este listener sabe tratar este tipo de evento
if (!$evento instanceof PedidoCriado) return;
echo "📧 Email enviado para {$evento->clienteEmail} (Pedido #{$evento->pedidoId})\n";
}
}
class AtualizarEstoque implements ListenerInterface
{
public function handle(object $evento): void
{
if (!$evento instanceof PedidoCriado) return;
echo "📦 Estoque reservado para o pedido #{$evento->pedidoId}\n";
}
}
class RegistrarLog implements ListenerInterface
{
public function handle(object $evento): void
{
// Este listener reage a QUALQUER evento — não precisa saber o tipo
$classe = get_class($evento);
echo "🗒️ [{$classe}] registrado em " . date('H:i:s') . "\n";
}
}
// EventDispatcher — o Subject / Publisher central da aplicação
final class EventDispatcher
{
/** @var array<string, ListenerInterface[]> */
private array $listeners = [];
// Registra um listener para uma classe de evento específica
public function listen(string $eventoClasse, ListenerInterface $listener): void
{
$this->listeners[$eventoClasse][] = $listener;
}
// Dispara o evento — todos os listeners registrados são notificados
public function dispatch(object $evento): void
{
$classe = get_class($evento);
// Notifica listeners específicos para este tipo de evento
foreach ($this->listeners[$classe] ?? [] as $listener) {
$listener->handle($evento);
}
// Notifica listeners curinga '*' — ouvem qualquer evento
foreach ($this->listeners['*'] ?? [] as $listener) {
$listener->handle($evento);
}
}
}
// Bootstrap — registra os listeners uma única vez
$dispatcher = new EventDispatcher();
$dispatcher->listen(PedidoCriado::class, new EnviarEmailConfirmacao());
$dispatcher->listen(PedidoCriado::class, new AtualizarEstoque());
$dispatcher->listen('*', new RegistrarLog());
// Disparar o evento — o criador do pedido não sabe quem está ouvindo
// e não precisa saber. Adicionar um novo listener não exige alterar este código
$dispatcher->dispatch(new PedidoCriado(42, 'maria@email.com', 350.0));
// 📧 Email enviado para maria@email.com (Pedido #42)
// 📦 Estoque reservado para o pedido #42
// 🗒️ [MeuApp\Eventos\PedidoCriado] registrado em 14:32:01
O sistema de Event::dispatch() do Laravel é exatamente este padrão. O EventServiceProvider.php mapeia eventos a listeners — equivalente ao nosso $dispatcher->listen(). O Laravel também suporta listeners em fila (implements ShouldQueue), que processam o evento de forma assíncrona, sem bloquear a resposta HTTP.
Decorator — adicionando comportamento sem herança
O Decorator envolve um objeto existente adicionando comportamentos antes ou depois das chamadas originais, sem modificar a classe base e sem usar herança. A chave do padrão está em uma condição fundamental: o decorator implementa a mesma interface que o objeto que envolve — o código cliente não percebe a diferença entre o objeto original e o decorado.
O caso mais concreto em PHP é o pipeline de middlewares HTTP: cada middleware envolve o próximo, adicionando autenticação, logging, cache ou compressão antes de passar a requisição adiante. Podem ser empilhados em qualquer ordem e combinados livremente. Com herança, adicionar cache e logging exigiria uma subclasse CachedLoggingRepository. Com Decorator, você compõe livremente — e adicionar um terceiro comportamento não exige alterar nenhuma classe existente.
<?php
declare(strict_types=1);
namespace MeuApp\Repositories;
// Interface compartilhada — o decorator e o objeto original
// implementam a mesma interface, tornando-os intercambiáveis para o cliente
interface ProdutoRepositoryInterface
{
public function buscarTodos(): array;
public function buscarPorId(int $id): ?array;
}
// Implementação concreta — a "folha" da cadeia, faz o trabalho real
class ProdutoRepository implements ProdutoRepositoryInterface
{
public function buscarTodos(): array
{
echo "🗄️ [DB] SELECT * FROM produtos\n";
return [
['id' => 1, 'nome' => 'Teclado', 'preco' => 350.0],
['id' => 2, 'nome' => 'Mouse', 'preco' => 180.0],
['id' => 3, 'nome' => 'Monitor', 'preco' => 1200.0],
];
}
public function buscarPorId(int $id): ?array
{
echo "🗄️ [DB] SELECT * FROM produtos WHERE id = {$id}\n";
return ['id' => $id, 'nome' => 'Produto', 'preco' => 100.0];
}
}
// Decorator base abstrato — guarda a referência ao objeto envolvido
// Subclasses herdam a delegação padrão e só sobrescrevem o que decoram
abstract class ProdutoRepositoryDecorator implements ProdutoRepositoryInterface
{
public function __construct(
protected readonly ProdutoRepositoryInterface $inner,
) {}
// Delegação padrão — repassa para o próximo na cadeia sem alterar nada
public function buscarTodos(): array { return $this->inner->buscarTodos(); }
public function buscarPorId(int $id): ?array { return $this->inner->buscarPorId($id); }
}
// Decorator 1 — Cache em memória
class CacheProdutoRepository extends ProdutoRepositoryDecorator
{
private array $cache = [];
public function buscarTodos(): array
{
if (!isset($this->cache['todos'])) {
echo "💾 [Cache MISS] buscando no banco...\n";
// Delega para o próximo na cadeia (pode ser o DB ou outro decorator)
$this->cache['todos'] = $this->inner->buscarTodos();
} else {
echo "⚡ [Cache HIT] retornando do cache\n";
}
return $this->cache['todos'];
}
public function buscarPorId(int $id): ?array
{
if (!isset($this->cache["id_{$id}"])) {
$this->cache["id_{$id}"] = $this->inner->buscarPorId($id);
}
return $this->cache["id_{$id}"];
}
}
// Decorator 2 — Logging de tempo de execução
class LoggingProdutoRepository extends ProdutoRepositoryDecorator
{
public function buscarTodos(): array
{
$inicio = hrtime(true);
$resultado = $this->inner->buscarTodos();
$ms = round((hrtime(true) - $inicio) / 1e6, 2);
echo "📋 [Log] buscarTodos: {$ms}ms — " . count($resultado) . " registros\n";
return $resultado;
}
}
// Composição — empilha decorators do mais externo para o mais interno
// A ordem importa: Log → Cache → DB
$repo = new LoggingProdutoRepository(
new CacheProdutoRepository(
new ProdutoRepository() // objeto base — faz a query real
)
);
// Primeira chamada: Log mede tempo → Cache MISS → DB executa
$produtos = $repo->buscarTodos();
// 📋 [Log] buscarTodos: X ms
// 💾 [Cache MISS] buscando no banco...
// 🗄️ [DB] SELECT * FROM produtos
// Segunda chamada: Log mede tempo → Cache HIT → sem consulta ao banco
$produtos = $repo->buscarTodos();
// 📋 [Log] buscarTodos: X ms
// ⚡ [Cache HIT] retornando do cache
Este é o Open/Closed Principle em ação: o ProdutoRepository original nunca foi modificado. Cache e logging foram adicionados por composição. Amanhã, um CompressionDecorator pode ser empilhado sem tocar em nenhuma das classes existentes.
Strategy — algoritmos intercambiáveis
O Strategy encapsula uma família de algoritmos em classes separadas, tornando-os intercambiáveis. O objeto que usa o algoritmo (o Context) recebe uma implementação de StrategyInterface — geralmente via construtor ou método setter — e a chama sem saber qual algoritmo concreto está executando. Isso elimina condicionais switch/if baseados em "tipo de algoritmo" espalhados pelo código.
O problema clássico que o Strategy resolve: imagine um sistema de e-commerce onde o cálculo de frete muda conforme o método escolhido pelo cliente. Sem Strategy, o código de Pedido teria um switch ($tipoFrete) com cada algoritmo embutido. Adicionar um novo tipo de frete exigiria modificar Pedido. Com Strategy, Pedido depende de uma interface — e cada algoritmo vive na sua própria classe.
<?php
declare(strict_types=1);
namespace MeuApp\Frete;
// Interface Strategy — define o contrato comum dos algoritmos
interface CalculadorFreteInterface
{
public function calcular(float $peso, float $distanciaKm): float;
public function prazoEntregaDias(): int;
public function nome(): string;
}
// Estratégias concretas — cada uma encapsula seu próprio algoritmo de cálculo
final class FreteCorreios implements CalculadorFreteInterface
{
public function calcular(float $peso, float $dist): float
{
// Fórmula simplificada: taxa base + peso + distância
return round(15.0 + ($peso * 2.5) + ($dist * 0.05), 2);
}
public function prazoEntregaDias(): int { return 7; }
public function nome(): string { return 'Correios PAC'; }
}
final class FreteExpress implements CalculadorFreteInterface
{
public function calcular(float $peso, float $dist): float
{
// Express tem tarifa mínima e é mais caro por kg e por km
return round(max(50.0, 30.0 + ($peso * 5.0) + ($dist * 0.12)), 2);
}
public function prazoEntregaDias(): int { return 1; }
public function nome(): string { return 'Express 24h'; }
}
final class FreteGratis implements CalculadorFreteInterface
{
public function calcular(float $peso, float $dist): float { return 0.0; }
public function prazoEntregaDias(): int { return 10; }
public function nome(): string { return 'Frete Grátis'; }
}
// Context — usa a estratégia injetada sem saber qual algoritmo é
class Pedido
{
public function __construct(
private readonly float $totalItens,
private readonly float $pesoKg,
private readonly float $distanciaKm,
// A estratégia é injetada — Pedido não conhece nenhuma classe concreta
private CalculadorFreteInterface $calculadorFrete,
) {}
// Permite trocar a estratégia em tempo de execução
public function setFrete(CalculadorFreteInterface $calculador): void
{
$this->calculadorFrete = $calculador;
}
public function valorFrete(): float
{
return $this->calculadorFrete->calcular($this->pesoKg, $this->distanciaKm);
}
public function resumo(): string
{
$frete = $this->valorFrete();
return sprintf(
"%s | Frete: R$ %.2f | Prazo: %d dia(s) | Total: R$ %.2f",
$this->calculadorFrete->nome(),
$frete,
$this->calculadorFrete->prazoEntregaDias(),
$this->totalItens + $frete
);
}
}
$pedido = new Pedido(
totalItens: 500.0,
pesoKg: 2.5,
distanciaKm: 400.0,
calculadorFrete: new FreteCorreios(),
);
echo $pedido->resumo() . "\n";
// Correios PAC | Frete: R$ 41.25 | Prazo: 7 dia(s) | Total: R$ 541.25
// Troca para Express sem modificar Pedido
$pedido->setFrete(new FreteExpress());
echo $pedido->resumo() . "\n";
// Express 24h | Frete: R$ 90.80 | Prazo: 1 dia(s) | Total: R$ 590.80
// Compra acima de R$1000 ganha frete grátis — regra de negócio externa ao Pedido
if ($pedido->totalItens >= 1000.0) {
$pedido->setFrete(new FreteGratis());
}
Para estratégias simples e de uso único, PHP permite passar uma Closure diretamente no lugar de um objeto com interface. Os drivers de fila (sync, database, redis), os guards de autenticação (session, token) e os drivers de cache do Laravel são todos Strategy — você troca uma linha no .env e o algoritmo inteiro muda sem modificar código de aplicação.
Os três padrões juntos — sistema de pagamentos
Em projetos reais, esses padrões frequentemente colaboram. Um processador de pagamentos usa Strategy para o método de cobrança, Decorator para adicionar retry e logging às chamadas, e Observer para notificar o restante do sistema quando um pagamento é concluído:
<?php
declare(strict_types=1);
// ── STRATEGY — define o algoritmo de cobrança ─────────────────────────
interface MetodoPagamento
{
public function cobrar(float $valor): bool;
public function nome(): string;
}
class PagamentoPix implements MetodoPagamento
{
public function cobrar(float $valor): bool
{
echo " ⚡ Pix: gerando QR Code para R$ {$valor}\n";
return true;
}
public function nome(): string { return 'Pix'; }
}
class PagamentoCartao implements MetodoPagamento
{
public function cobrar(float $valor): bool
{
echo " 💳 Cartão: autorizando R$ {$valor}\n";
return true;
}
public function nome(): string { return 'Cartão'; }
}
// ── DECORATOR — adiciona retry sem modificar as classes de pagamento ───
class PagamentoComRetry implements MetodoPagamento
{
public function __construct(
private readonly MetodoPagamento $inner,
private readonly int $tentativas = 3,
) {}
public function cobrar(float $valor): bool
{
for ($i = 1; $i <= $this->tentativas; $i++) {
echo " 🔄 Tentativa {$i}/{$this->tentativas}\n";
if ($this->inner->cobrar($valor)) return true;
}
return false;
}
public function nome(): string { return $this->inner->nome() . ' (+retry)'; }
}
// ── OBSERVER — notifica o sistema após o pagamento ────────────────────
final class PagamentoProcessado
{
public function __construct(
public readonly string $metodo,
public readonly float $valor,
public readonly bool $sucesso,
) {}
}
// Context que usa Strategy e dispara Observer ao concluir
class Checkout
{
private array $onPagamento = [];
public function __construct(private MetodoPagamento $metodo) {}
public function aoProcessar(\Closure $callback): void
{
$this->onPagamento[] = $callback;
}
public function pagar(float $valor): bool
{
echo "🛒 Processando R$ {$valor} via {$this->metodo->nome()}\n";
$sucesso = $this->metodo->cobrar($valor);
// Dispara o evento para todos os observers registrados
$evento = new PagamentoProcessado($this->metodo->nome(), $valor, $sucesso);
foreach ($this->onPagamento as $cb) $cb($evento);
return $sucesso;
}
}
// Composição final: Strategy dentro de Decorator, Context com Observer
$checkout = new Checkout(
new PagamentoComRetry(new PagamentoPix(), 2)
);
$checkout->aoProcessar(fn(PagamentoProcessado $e) =>
print(" 📧 Recibo de R$ {$e->valor} enviado ao cliente\n")
);
$checkout->aoProcessar(fn(PagamentoProcessado $e) =>
print(" 📊 Transação registrada no financeiro\n")
);
$checkout->pagar(350.0);
// 🛒 Processando R$ 350 via Pix (+retry)
// 🔄 Tentativa 1/2
// ⚡ Pix: gerando QR Code para R$ 350
// 📧 Recibo de R$ 350 enviado ao cliente
// 📊 Transação registrada no financeiro
Resumo do artigo
| Padrão | Categoria | Problema resolvido | Onde aparece em frameworks |
|---|---|---|---|
| Observer | Comportamental | Desacoplar quem dispara de quem reage a eventos | Event::dispatch() no Laravel, EventDispatcher do Symfony |
| Decorator | Estrutural | Adicionar comportamentos a objetos sem herança | Middlewares HTTP, cache layers, stream wrappers |
| Strategy | Comportamental | Algoritmos intercambiáveis com a mesma interface | Auth guards, drivers de fila, drivers de cache, mailers |
Exercício da semana
-
Implemente um
EventDispatchercom suporte a prioridade nos listeners — listeners de prioridade mais alta são notificados primeiro. Adicione tambémremoveListener(string $evento, ListenerInterface $listener): void. -
Crie uma cadeia de Decorators para
TextoProcessadorInterfacecom métodoprocessar(string $texto): string. Implemente a classe base e três decorators:UpperCaseDecorator,TrimDecoratoreSlugDecorator(espaços viram hífens, remove acentos e caracteres especiais). Teste-os empilhados em ordens diferentes e observe como a ordem altera o resultado. -
Implemente a Strategy de autenticação: interface
AuthStrategyInterfacecomautenticar(string $credencial): booletipo(): string. ImplementeAuthPassword,AuthTokeneAuthApiKey. UmAuthManagerrecebe a estratégia via construtor. -
Combine os três padrões: um
PaginaServicebusca conteúdo do banco, decorado com cache e logging. Quando o conteúdo é acessado pela primeira vez, dispara um eventoConteudoAcessado. Dois listeners registram o acesso e enviam dados de analytics. -
Desafio: implemente um mini-pipeline de transformação. Interface
TransformacaoInterfacecomtransformar(array $dados): arraycomo Strategy. Uma classePipelinerecebe um array de transformações e executa em sequência. Cada transformação é decorada com logging antes e depois. Quando o pipeline termina, disparaTransformacaoConcluidavia Observer.
Referências
- GAMMA et al. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. https://www.oreilly.com/library/view/design-patterns-elements/0201633612/
- Refactoring Guru — Observer (pt-BR): https://refactoring.guru/pt-br/design-patterns/observer
- Refactoring Guru — Decorator (pt-BR): https://refactoring.guru/pt-br/design-patterns/decorator
- Refactoring Guru — Strategy (pt-BR): https://refactoring.guru/pt-br/design-patterns/strategy
- Laravel Events — Documentação oficial: https://laravel.com/docs/events
- Symfony HttpKernel — Middleware como Decorator: https://symfony.com/doc/current/http_kernel.html
- Design Patterns PHP (pt-BR): https://designpatternsphp.readthedocs.io/pt_BR/latest/