Observer, Decorator e Strategy

Observer, Decorator e Strategy

Três padrões que aparecem em praticamente qualquer sistema: o Observer, que avisa sem acoplar; o Decorator, que acrescenta comportamento sem herdar; e o Strategy, que troca o algoritmo sem tocar em quem o usa. Cada um com o problema que resolve e o custo que cobra em indireção.
PHP

24 min de leitura

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

Padrão de projeto não é receita a aplicar, é nome dado a uma solução que já existia. O valor prático está menos em implementar os três corretamente e mais em reconhecê-los no código alheio: quando alguém diz "isso aqui é um Strategy", uma classe inteira de dúvidas deixa de precisar ser explicada. O contrário também vale — introduzir um dos três antes de existir a segunda variação do problema costuma comprar indireção sem comprar flexibilidade nenhuma.

Fontes e leituras recomendadas

Exercícios

Exercício 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.

Ver resposta

✓ Resposta: Dois detalhes que separam o brinquedo do utilizável: o campo ordem, que torna a ordenação estável entre listeners de mesma prioridade, e o !== na remoção, que compara identidade de objeto em vez de igualdade de estado.

<?php

declare(strict_types=1);

interface ListenerInterface
{
    public function handle(string $evento, array $dados): void;
}

final class EventDispatcher
{
    /** @var array<string, array<int, array{listener: ListenerInterface, prioridade: int, ordem: int}>> */
    private array $listeners = [];

    private int $sequencia = 0;

    public function addListener(
        string $evento,
        ListenerInterface $listener,
        int $prioridade = 0,
    ): void {
        $this->listeners[$evento][] = [
            'listener'   => $listener,
            'prioridade' => $prioridade,
            // Desempate estável: com a mesma prioridade, vence quem chegou antes.
            // Sem isso, a ordem de dois listeners iguais fica indefinida.
            'ordem'      => $this->sequencia++,
        ];
    }

    public function removeListener(string $evento, ListenerInterface $listener): void
    {
        if (!isset($this->listeners[$evento])) {
            return;
        }

        $this->listeners[$evento] = array_values(array_filter(
            $this->listeners[$evento],
            // Comparação por identidade (===): remove aquele objeto, não
            // qualquer outro que por acaso tenha o mesmo estado.
            static fn(array $r): bool => $r['listener'] !== $listener,
        ));

        if ($this->listeners[$evento] === []) {
            unset($this->listeners[$evento]);
        }
    }

    public function dispatch(string $evento, array $dados = []): void
    {
        $registros = $this->listeners[$evento] ?? [];

        usort($registros, static fn(array $a, array $b): int =>
            [$b['prioridade'], -$a['ordem']] <=> [$a['prioridade'], -$b['ordem']]
        );

        foreach ($registros as $registro) {
            $registro['listener']->handle($evento, $dados);
        }
    }
}

// Uso:
final class ListenerNomeado implements ListenerInterface
{
    public function __construct(private readonly string $nome) {}

    public function handle(string $evento, array $dados): void
    {
        echo "  {$this->nome} tratou {$evento}", PHP_EOL;
    }
}

$dispatcher = new EventDispatcher();

$auditoria = new ListenerNomeado('auditoria');
$dispatcher->addListener('pedido.pago', new ListenerNomeado('email'), prioridade: 0);
$dispatcher->addListener('pedido.pago', $auditoria, prioridade: 100);
$dispatcher->addListener('pedido.pago', new ListenerNomeado('estoque'), prioridade: 50);

$dispatcher->dispatch('pedido.pago', ['id' => 42]);
//   auditoria tratou pedido.pago     (prioridade 100)
//   estoque tratou pedido.pago       (prioridade 50)
//   email tratou pedido.pago         (prioridade 0)

$dispatcher->removeListener('pedido.pago', $auditoria);
$dispatcher->dispatch('pedido.pago', ['id' => 43]);
//   estoque tratou pedido.pago
//   email tratou pedido.pago

Exercício 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.

Ver resposta

✓ Resposta: A ordem importa porque cada decorator recebe a saída do anterior, não o texto original. O par A/B mostra: se o UpperCase vem depois do Slug, o resultado é um slug em maiúsculas — tecnicamente correto e completamente inútil como URL.

<?php

declare(strict_types=1);

interface TextoProcessadorInterface
{
    public function processar(string $texto): string;
}

// O componente concreto: não faz nada, só devolve. É o fim da cadeia.
final class TextoSimples implements TextoProcessadorInterface
{
    public function processar(string $texto): string
    {
        return $texto;
    }
}

// A base: guarda o próximo da cadeia e delega. Todo decorator estende isto.
abstract class TextoDecorator implements TextoProcessadorInterface
{
    public function __construct(
        protected readonly TextoProcessadorInterface $interno,
    ) {}

    public function processar(string $texto): string
    {
        return $this->interno->processar($texto);
    }
}

final class UpperCaseDecorator extends TextoDecorator
{
    public function processar(string $texto): string
    {
        // mb_strtoupper, não strtoupper: "ação" tem de virar "AÇÃO".
        return mb_strtoupper(parent::processar($texto), 'UTF-8');
    }
}

final class TrimDecorator extends TextoDecorator
{
    public function processar(string $texto): string
    {
        // Corta as pontas e colapsa espaço interno repetido.
        return preg_replace('/\s+/u', ' ', trim(parent::processar($texto)));
    }
}

final class SlugDecorator extends TextoDecorator
{
    public function processar(string $texto): string
    {
        $texto = parent::processar($texto);

        // Remove acento transliterando para ASCII.
        $ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $texto);

        $ascii = strtolower($ascii);
        $ascii = preg_replace('/[^a-z0-9]+/', '-', $ascii);

        return trim($ascii, '-');
    }
}

$entrada = '   Programação   Orientada a Objetos   ';

// Ordem A: limpa, sobe a caixa, depois faz o slug.
$a = new SlugDecorator(new UpperCaseDecorator(new TrimDecorator(new TextoSimples())));
echo $a->processar($entrada), PHP_EOL;
// programacao-orientada-a-objetos

// Ordem B: faz o slug ANTES de subir a caixa.
$b = new UpperCaseDecorator(new SlugDecorator(new TrimDecorator(new TextoSimples())));
echo $b->processar($entrada), PHP_EOL;
// PROGRAMACAO-ORIENTADA-A-OBJETOS

// Ordem C: sem o Trim, o espaço das pontas vira hífen sobrando.
$c = new SlugDecorator(new TextoSimples());
echo '[', $c->processar($entrada), ']', PHP_EOL;
// [programacao-orientada-a-objetos]  ← o trim(…, '-') do slug salvou as pontas

Exercício 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.

Ver resposta

✓ Resposta: Repare que as três estratégias comparam segredo com hash_equals ou password_verify, nunca com ===. A comparação comum sai mais cedo no primeiro byte diferente, e essa diferença de tempo é mensurável — dá para descobrir o segredo byte a byte.

<?php

declare(strict_types=1);

interface AuthStrategyInterface
{
    public function autenticar(string $credencial): bool;

    public function tipo(): string;
}

final class AuthPassword implements AuthStrategyInterface
{
    public function __construct(private readonly string $hashArmazenado) {}

    public function autenticar(string $credencial): bool
    {
        // password_verify já é resistente a timing attack.
        return password_verify($credencial, $this->hashArmazenado);
    }

    public function tipo(): string { return 'password'; }
}

final class AuthToken implements AuthStrategyInterface
{
    public function __construct(
        private readonly string $tokenValido,
        private readonly DateTimeImmutable $expiraEm,
    ) {}

    public function autenticar(string $credencial): bool
    {
        if (new DateTimeImmutable() > $this->expiraEm) {
            return false;
        }

        // hash_equals compara em tempo constante: comparar segredo com ===
        // vaza informação pelo tempo de resposta.
        return hash_equals($this->tokenValido, $credencial);
    }

    public function tipo(): string { return 'token'; }
}

final class AuthApiKey implements AuthStrategyInterface
{
    /** @param array<string, string> $chaves  chave => nome do cliente */
    public function __construct(private readonly array $chaves) {}

    public function autenticar(string $credencial): bool
    {
        foreach ($this->chaves as $chave => $cliente) {
            if (hash_equals((string) $chave, $credencial)) {
                return true;
            }
        }

        return false;
    }

    public function tipo(): string { return 'api-key'; }
}

final class AuthManager
{
    public function __construct(private readonly AuthStrategyInterface $estrategia) {}

    public function login(string $credencial): string
    {
        $ok = $this->estrategia->autenticar($credencial);

        return sprintf(
            '[%s] %s',
            $this->estrategia->tipo(),
            $ok ? 'autenticado' : 'recusado',
        );
    }
}

// Uso: o AuthManager é o mesmo nos três casos.
$hash = password_hash('segredo123', PASSWORD_DEFAULT);

echo (new AuthManager(new AuthPassword($hash)))->login('segredo123'), PHP_EOL;
// [password] autenticado

echo (new AuthManager(new AuthToken('abc123', new DateTimeImmutable('+1 hour'))))
        ->login('abc123'), PHP_EOL;
// [token] autenticado

echo (new AuthManager(new AuthApiKey(['k-live-9f2' => 'App Mobile'])))
        ->login('k-errada'), PHP_EOL;
// [api-key] recusado

Exercício 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.

Ver resposta

✓ Resposta: O enunciado pede que o evento dispare "na primeira vez" — e a solução não usa nenhuma flag para isso. O evento mora no serviço real, e o cache, por estar por fora, simplesmente impede a segunda chamada de chegar lá. Ordem de empilhamento é comportamento.

<?php

declare(strict_types=1);

interface PaginaServiceInterface
{
    public function buscar(string $slug): ?string;
}

// ---------- O componente real: só ele fala com o banco --------------------
final class PaginaService implements PaginaServiceInterface
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly EventDispatcher $eventos,
    ) {}

    public function buscar(string $slug): ?string
    {
        $stmt = $this->pdo->prepare('SELECT conteudo FROM paginas WHERE slug = :slug');
        $stmt->execute([':slug' => $slug]);

        $conteudo = $stmt->fetchColumn();

        if ($conteudo === false) {
            return null;
        }

        // O evento sai daqui — do acesso REAL. Como o cache decora este
        // serviço, o segundo acesso não chega até aqui e não redispara.
        $this->eventos->dispatch('ConteudoAcessado', [
            'slug'   => $slug,
            'quando' => new DateTimeImmutable(),
        ]);

        return (string) $conteudo;
    }
}

// ---------- Decorator 1: cache -------------------------------------------
final class PaginaCacheDecorator implements PaginaServiceInterface
{
    private array $cache = [];

    public function __construct(private readonly PaginaServiceInterface $interno) {}

    public function buscar(string $slug): ?string
    {
        // array_key_exists, não isset: uma página inexistente guarda null,
        // e isset(null) é false — o cache nunca pegaria o "não existe".
        if (array_key_exists($slug, $this->cache)) {
            return $this->cache[$slug];
        }

        return $this->cache[$slug] = $this->interno->buscar($slug);
    }
}

// ---------- Decorator 2: logging -----------------------------------------
final class PaginaLogDecorator implements PaginaServiceInterface
{
    public function __construct(private readonly PaginaServiceInterface $interno) {}

    public function buscar(string $slug): ?string
    {
        $inicio = hrtime(true);
        $resultado = $this->interno->buscar($slug);
        $ms = (hrtime(true) - $inicio) / 1_000_000;

        printf("[log] buscar('%s') = %s em %.2f ms%s",
               $slug, $resultado === null ? 'null' : 'hit', $ms, PHP_EOL);

        return $resultado;
    }
}

// ---------- Observers -----------------------------------------------------
final class RegistrarAcesso implements ListenerInterface
{
    public function handle(string $evento, array $dados): void
    {
        printf("  [acesso] %s em %s%s",
               $dados['slug'], $dados['quando']->format('H:i:s'), PHP_EOL);
    }
}

final class EnviarAnalytics implements ListenerInterface
{
    public function handle(string $evento, array $dados): void
    {
        printf("  [analytics] page_view slug=%s%s", $dados['slug'], PHP_EOL);
    }
}

// ---------- Montagem ------------------------------------------------------
$eventos = new EventDispatcher();
$eventos->addListener('ConteudoAcessado', new RegistrarAcesso(), prioridade: 10);
$eventos->addListener('ConteudoAcessado', new EnviarAnalytics());

// A ordem do empilhamento define o que mede o quê: aqui o log fica POR FORA
// do cache, então ele mostra o ganho do cache no tempo.
$servico = new PaginaLogDecorator(
    new PaginaCacheDecorator(
        new PaginaService($pdo, $eventos)
    )
);

$servico->buscar('sobre-nos');   // vai ao banco, dispara o evento
$servico->buscar('sobre-nos');   // vem do cache, NÃO dispara o evento

// [log] buscar('sobre-nos') = hit em 4.13 ms
//   [acesso] sobre-nos em 14:30:12
//   [analytics] page_view slug=sobre-nos
// [log] buscar('sobre-nos') = hit em 0.01 ms

Exercício 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.

Ver resposta

✓ Resposta: O Pipeline não sabe se uma transformação está logada ou não — recebe TransformacaoInterface e chama. É o que permite ligar e desligar o log só mudando a montagem, sem tocar em nenhuma das três transformações.

<?php

declare(strict_types=1);

// ---------- STRATEGY: cada transformação é intercambiável -----------------
interface TransformacaoInterface
{
    public function transformar(array $dados): array;

    public function nome(): string;
}

final class RemoverVazios implements TransformacaoInterface
{
    public function transformar(array $dados): array
    {
        return array_values(array_filter(
            $dados,
            static fn(array $linha): bool => trim((string) ($linha['nome'] ?? '')) !== '',
        ));
    }

    public function nome(): string { return 'remover-vazios'; }
}

final class NormalizarNomes implements TransformacaoInterface
{
    public function transformar(array $dados): array
    {
        return array_map(static function (array $linha): array {
            $linha['nome'] = mb_convert_case(
                trim($linha['nome']), MB_CASE_TITLE, 'UTF-8'
            );
            return $linha;
        }, $dados);
    }

    public function nome(): string { return 'normalizar-nomes'; }
}

final class ConverterCentavos implements TransformacaoInterface
{
    public function transformar(array $dados): array
    {
        return array_map(static function (array $linha): array {
            $linha['preco'] = (int) round(((float) $linha['preco']) * 100);
            return $linha;
        }, $dados);
    }

    public function nome(): string { return 'converter-centavos'; }
}

// ---------- DECORATOR: log antes e depois, sem tocar nas transformações ---
final class TransformacaoLogada implements TransformacaoInterface
{
    public function __construct(private readonly TransformacaoInterface $interno) {}

    public function transformar(array $dados): array
    {
        $antes = count($dados);
        $inicio = hrtime(true);

        printf("  → %-20s entrada: %d linha(s)%s", $this->interno->nome(), $antes, PHP_EOL);

        $saida = $this->interno->transformar($dados);

        printf("  ← %-20s saída:   %d linha(s) em %.2f ms%s",
               $this->interno->nome(), count($saida),
               (hrtime(true) - $inicio) / 1_000_000, PHP_EOL);

        return $saida;
    }

    public function nome(): string { return $this->interno->nome(); }
}

// ---------- OBSERVER: avisa quando o pipeline termina ---------------------
final class Pipeline
{
    /** @param TransformacaoInterface[] $transformacoes */
    public function __construct(
        private readonly array $transformacoes,
        private readonly EventDispatcher $eventos,
    ) {}

    public function executar(array $dados): array
    {
        $inicio = hrtime(true);
        $entrada = count($dados);

        // O coração do pipeline: a saída de uma alimenta a próxima.
        foreach ($this->transformacoes as $transformacao) {
            $dados = $transformacao->transformar($dados);
        }

        $this->eventos->dispatch('TransformacaoConcluida', [
            'entrada' => $entrada,
            'saida'   => count($dados),
            'ms'      => (hrtime(true) - $inicio) / 1_000_000,
            'etapas'  => array_map(
                static fn(TransformacaoInterface $t): string => $t->nome(),
                $this->transformacoes,
            ),
        ]);

        return $dados;
    }
}

final class ResumoPipeline implements ListenerInterface
{
    public function handle(string $evento, array $dados): void
    {
        printf("✔ %s: %d → %d linha(s) em %.2f ms [%s]%s",
               $evento, $dados['entrada'], $dados['saida'], $dados['ms'],
               implode(' > ', $dados['etapas']), PHP_EOL);
    }
}

// ---------- Uso -----------------------------------------------------------
$eventos = new EventDispatcher();
$eventos->addListener('TransformacaoConcluida', new ResumoPipeline());

// Cada Strategy entra embrulhada no Decorator de log.
$pipeline = new Pipeline([
    new TransformacaoLogada(new RemoverVazios()),
    new TransformacaoLogada(new NormalizarNomes()),
    new TransformacaoLogada(new ConverterCentavos()),
], $eventos);

$resultado = $pipeline->executar([
    ['nome' => '  teclado mecânico ', 'preco' => '349.90'],
    ['nome' => '',                    'preco' => '10.00'],
    ['nome' => 'MOUSE sem fio',       'preco' => '129.5'],
]);

print_r($resultado);
// [ ['nome' => 'Teclado Mecânico', 'preco' => 34990],
//   ['nome' => 'Mouse Sem Fio',    'preco' => 12950] ]
Comentários

Mais em PHP

Interfaces Avançadas
Interfaces Avançadas

Interface é contrato, e contrato tem regras que vão além de listar métodos…

O que é PHP e por que ele ainda importa
O que é PHP e por que ele ainda importa

PHP roda no servidor, e é essa única característica que explica quase tudo…

Orientação a Objetos: Fundamentos
Orientação a Objetos: Fundamentos

Uma classe define o molde; cada objeto guarda o próprio estado. Construtor…