Design Pattern

Padrão de Projeto Decorator em PHP com exemplo

Capa do artigo Padrão de Projeto Decorator em PHP com exemplo
Leitura com foco

Entenda o contexto.
Leve para a prática.

Neste artigo
  1. Qual problema o Decorator resolve?
  2. Exemplo do padrão Decorator em PHP
  3. Testando o exemplo
  4. Quando usar o padrão Decorator?
  5. Cuidados ao usar o Decorator
  6. Decorator, Proxy e Adapter: qual a diferença?

O Padrão de Projeto Decorator permite acrescentar responsabilidades a um objeto em tempo de execução, sem alterar a classe dele e sem usar herança. Ele faz isso “embrulhando” o objeto original em outro objeto que implementa a mesma interface: o decorador recebe a chamada, repassa para o objeto embrulhado e acrescenta o seu comportamento antes ou depois.

Como o decorador tem a mesma interface do objeto que ele embrulha, quem usa não percebe a diferença. E como um decorador pode embrulhar outro decorador, dá para empilhar quantos você precisar. É por isso que esse padrão também é conhecido como Wrapper.

Diagrama de classe UML do padrão de projeto Decorator
Diagrama de classe UML do padrão de projeto Decorator

Observe no diagrama os quatro participantes do padrão:

  • Component (Notifier): a interface comum ao objeto original e aos decoradores.
  • ConcreteComponent (EmailNotifier): o objeto que vai receber as responsabilidades novas.
  • Decorator (NotifierDecorator): guarda uma referência a um Component e repassa as chamadas para ele.
  • ConcreteDecorator (SmsNotifier e SlackNotifier): acrescenta a responsabilidade.

Qual problema o Decorator resolve?

Imagine um sistema que avisa a equipe quando um deploy termina. No começo o aviso vai por e-mail, e uma classe resolve. Depois alguém pede SMS. Depois, Slack. E cada pessoa quer uma combinação diferente.

Se você resolver isso com herança, vai precisar de uma classe para cada combinação: e-mail com SMS, e-mail com Slack, e-mail com SMS e Slack. Com dois canais opcionais são 4 combinações; com três, 8; e cada canal novo dobra esse número. Com o Decorator, cada canal vira uma classe pequena, e a combinação é montada na hora, de acordo com a preferência de cada usuário.

Exemplo do padrão Decorator em PHP

Vamos para o código. O contrato é simples: todo notificador sabe enviar uma mensagem.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Decorator;

interface Notifier
{
    public function send(string $message): void;
}

O EmailNotifier é o componente concreto. Ele faz o trabalho básico e não sabe que pode ser decorado.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Decorator;

final class EmailNotifier implements Notifier
{
    private readonly string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }

    public function send(string $message): void
    {
        printf("E-mail to %s: %s\n", $this->email, $message);
    }
}

Agora vem a peça central do padrão. O NotifierDecorator implementa a interface Notifier e, ao mesmo tempo, guarda um Notifier dentro dele. A única coisa que ele faz é repassar a chamada.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Decorator;

abstract class NotifierDecorator implements Notifier
{
    private readonly Notifier $notifier;

    public function __construct(Notifier $notifier)
    {
        $this->notifier = $notifier;
    }

    public function send(string $message): void
    {
        $this->notifier->send($message);
    }
}

Cada decorador concreto estende essa classe base, chama o parent::send() para manter o que já existia e acrescenta o seu canal.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Decorator;

final class SmsNotifier extends NotifierDecorator
{
    private readonly string $phone;

    public function __construct(Notifier $notifier, string $phone)
    {
        parent::__construct($notifier);
        $this->phone = $phone;
    }

    public function send(string $message): void
    {
        parent::send($message);

        printf("SMS to %s: %s\n", $this->phone, $message);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Decorator;

final class SlackNotifier extends NotifierDecorator
{
    private readonly string $channel;

    public function __construct(Notifier $notifier, string $channel)
    {
        parent::__construct($notifier);
        $this->channel = $channel;
    }

    public function send(string $message): void
    {
        parent::send($message);

        printf("Slack to %s: %s\n", $this->channel, $message);
    }
}

Repare que nenhuma dessas classes conhece as outras. O SmsNotifier não sabe se está embrulhando um e-mail, um Slack ou outro SMS. Ele só sabe que tem um Notifier nas mãos.

Testando o exemplo

Nos testes dá para ver o padrão funcionando em três situações: sem nenhum decorador, com dois decoradores empilhados e com os decoradores escolhidos em tempo de execução, a partir das preferências do usuário.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Structural\Decorator;

use Growthdev\DesignPatterns\Structural\Decorator\EmailNotifier;
use Growthdev\DesignPatterns\Structural\Decorator\Notifier;
use Growthdev\DesignPatterns\Structural\Decorator\SlackNotifier;
use Growthdev\DesignPatterns\Structural\Decorator\SmsNotifier;
use PHPUnit\Framework\TestCase;

final class DecoratorTest extends TestCase
{
    public function testExpectOnlyEmailWithoutDecorators(): void
    {
        $notifier = new EmailNotifier('dev@example.com');
        $notifier->send('Deploy finished');

        $this->expectOutputString("E-mail to dev@example.com: Deploy finished\n");
    }

    public function testExpectEachDecoratorToAddOneChannel(): void
    {
        $notifier = new SlackNotifier(
            new SmsNotifier(new EmailNotifier('dev@example.com'), '+55 11 90000-0000'),
            '#deploys'
        );
        $notifier->send('Deploy finished');

        $this->expectOutputString(
            "E-mail to dev@example.com: Deploy finished\n"
            . "SMS to +55 11 90000-0000: Deploy finished\n"
            . "Slack to #deploys: Deploy finished\n"
        );
    }

    public function testExpectDecoratorsToBeChosenAtRuntime(): void
    {
        $preferences = ['slack' => '#deploys'];

        $notifier = new EmailNotifier('dev@example.com');

        if (isset($preferences['sms'])) {
            $notifier = new SmsNotifier($notifier, $preferences['sms']);
        }

        if (isset($preferences['slack'])) {
            $notifier = new SlackNotifier($notifier, $preferences['slack']);
        }

        $this->assertInstanceOf(Notifier::class, $notifier);

        $notifier->send('Deploy finished');

        $this->expectOutputString(
            "E-mail to dev@example.com: Deploy finished\n"
            . "Slack to #deploys: Deploy finished\n"
        );
    }
}

No segundo teste, a ordem da saída é e-mail, SMS e Slack. Isso acontece porque cada decorador chama primeiro o objeto que está embrulhando e só depois faz a sua parte. Se você inverter a ordem em que embrulha, a ordem do envio muda junto. No terceiro teste, o usuário só pediu Slack, então o SmsNotifier nem chega a ser criado.

Quando usar o padrão Decorator?

  • Quando as responsabilidades são opcionais e podem ser combinadas de várias formas.
  • Quando você não pode ou não quer mexer na classe original: ela é final, vem de uma biblioteca de terceiros ou já está bem testada.
  • Quando a responsabilidade precisa ser ligada e desligada em tempo de execução.

No ecossistema PHP você encontra esse padrão com frequência. O Symfony tem a decoração de serviços no container de injeção de dependência, e o pacote guzzlehttp/psr7 traz decoradores de stream prontos.

Cuidados ao usar o Decorator

  • Muitas camadas pequenas dificultam a depuração: o erro pode estar em qualquer uma delas.
  • A ordem dos decoradores muda o resultado, como você viu no teste.
  • O objeto decorado não é o objeto original. Código que depende da classe concreta, como um instanceof EmailNotifier, deixa de funcionar quando o objeto está embrulhado.

Decorator, Proxy e Adapter: qual a diferença?

Os três embrulham um objeto, e é por isso que são confundidos. A diferença está na intenção de cada um:

PadrãoInterface que expõeIntenção
DecoratorA mesma do objeto embrulhadoAcrescentar comportamento
ProxyA mesma do objeto embrulhadoControlar o acesso ao objeto
AdapterUma interface diferenteFazer duas interfaces incompatíveis trabalharem juntas
Diferença entre Decorator, Proxy e Adapter.

Esse é mais um padrão estrutural do livro GoF. Para conhecer os outros, veja o artigo Resumo dos Padrões de Projetos.

O código completo deste exemplo, com os testes, está no meu GitHub, na pasta src/Structural/Decorator do repositório growthdev-repo/design-patterns.

Veja também: o resumo dos 23 padrões de projeto GoF e os outros padrões estruturais já publicados: Adapter, Bridge, Composite, Facade, Flyweight, Proxy.

Confiança Sempre!!!

Fontes:

  • [1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.
  • [2] FREEMAN, Eric; ROBSON, Elisabeth; BATES, Bert; SIERRA, Kathy. Head First Design Patterns.
Sobre quem escreve

Walmir Silva

Olá! Sou Walmir, engenheiro de software com MBA em Engenharia de Software e o cérebro por trás do GrowthCode e autor do livro "Além do Código".

Se você acha que programação é apenas sobre escrever código, prepare-se para expandir seus horizontes. Aqui, nós vamos além do código e exploramos as interseções fascinantes entre tecnologia, negócios, artes e filosofia.

Você está em busca de crescimento na carreira? Quer se destacar em um mercado competitivo? Almeja uma vida mais rica em conhecimento e realização? Então você chegou ao lugar certo. No GrowthCode, oferecemos insights profundos, estratégias comprovadas e um toque de sabedoria filosófica para catalisar seu crescimento pessoal e profissional.

Ver todos os artigos →

Continue a conversa

Compartilhe sua perspectiva

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Encontre sua próxima resposta

O que você quer aprender?

Preferências de cookies

Os cookies opcionais começam desligados. Você pode mudar a escolha a qualquer momento por esta tela.

NecessáriosSempre ativos

Guardam a sua escolha de cookies e mantêm o site funcionando. Não são usados para publicidade.

Contam visitas e mostram como o site é usado: Google Analytics e Microsoft Clarity, carregados pelo Google Tag Manager. Como esse contêiner também carrega tags de publicidade, as estatísticas só são ativadas junto com Publicidade e marketing.

Exibem anúncios do Google AdSense e medem campanhas e e-mails: pixels da Meta e do TikTok e o Mautic.

Permite que o Google escolha anúncios com base nos seus interesses. Desligado, os anúncios aparecem sem personalização. Depende de Publicidade e marketing.

Também é possível bloquear cookies nas configurações do navegador. Detalhes na Política de privacidade.