PHP

Observer, Decorator e Strategy Já leu

14 min de leitura

Observer, Decorator e Strategy
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 (

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

  1. Implemente um EventDispatcher com suporte a prioridade nos listeners — listeners de prioridade mais alta são notificados primeiro. Adicione também removeListener(string $evento, ListenerInterface $listener): void.

  2. Crie uma cadeia de Decorators para TextoProcessadorInterface com método processar(string $texto): string. Implemente a classe base e três decorators: UpperCaseDecorator, TrimDecorator e SlugDecorator (espaços viram hífens, remove acentos e caracteres especiais). Teste-os empilhados em ordens diferentes e observe como a ordem altera o resultado.

  3. Implemente a Strategy de autenticação: interface AuthStrategyInterface com autenticar(string $credencial): bool e tipo(): string. Implemente AuthPassword, AuthToken e AuthApiKey. Um AuthManager recebe a estratégia via construtor.

  4. Combine os três padrões: um PaginaService busca conteúdo do banco, decorado com cache e logging. Quando o conteúdo é acessado pela primeira vez, dispara um evento ConteudoAcessado. Dois listeners registram o acesso e enviam dados de analytics.

  5. Desafio: implemente um mini-pipeline de transformação. Interface TransformacaoInterface com transformar(array $dados): array como Strategy. Uma classe Pipeline recebe um array de transformações e executa em sequência. Cada transformação é decorada com logging antes e depois. Quando o pipeline termina, dispara TransformacaoConcluida via Observer.


Referências

Comentários

Mais em PHP

Domain-Driven Design
Domain-Driven Design

Domain-Driven Design (DDD) é uma abordagem para desenvolver software complexo...

Artigo 49 — Performance e Otimização
Artigo 49 — Performance e Otimização

Performance não é premature optimization — é entender onde estão os gargalos...

A História do PHP: de script pessoal a pilar da web
A História do PHP: de script pessoal a pilar da web

Poucas linguagens de programa&ccedil;&atilde;o t&ecirc;m uma origem t&atilde;...