Design Pattern

Padrão de Projeto Bridge em PHP com exemplo

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

Entenda o contexto.
Leve para a prática.

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

O Padrão de Projeto Bridge separa uma abstração da sua implementação, para que as duas possam mudar de forma independente. Em vez de uma única hierarquia de classes que cresce em duas direções, você cria duas hierarquias menores e liga uma à outra por composição.

O nome vem dessa ligação: o objeto de um lado guarda uma referência para o objeto do outro, e essa referência é a ponte (bridge, em inglês).

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

Observe no diagrama os participantes do padrão:

  • Abstraction (Notification): a classe que o cliente usa. Guarda a referência para a implementação.
  • RefinedAbstraction (OrderShippedNotification e PaymentFailedNotification): as variações da abstração.
  • Implementor (Channel): a interface do lado da implementação.
  • ConcreteImplementor (EmailChannel e SmsChannel): as implementações concretas.

Qual problema o Bridge resolve?

Pense em um sistema que avisa o cliente sobre o pedido. Existem tipos de aviso (pedido enviado, pagamento recusado) e existem canais de envio (e-mail, SMS). São duas coisas que mudam por motivos diferentes: o tipo de aviso muda quando o negócio cria uma regra nova, e o canal muda quando a empresa contrata um serviço novo.

A solução com herança cria uma classe para cada combinação:

<?php

class OrderShippedEmail {}
class OrderShippedSms {}
class PaymentFailedEmail {}
class PaymentFailedSms {}

// Um canal novo (push) exige mais duas classes:
class OrderShippedPush {}
class PaymentFailedPush {}

Com dois tipos e dois canais são quatro classes, e o código de envio por e-mail fica repetido em duas delas. Cada tipo ou canal novo aumenta o problema:

Tipos de avisoCanaisClasses com herançaClasses com Bridge
2244
3396
45209
Com herança as quantidades se multiplicam; com Bridge, elas se somam. A conta não inclui a classe base e a interface.

Com o Bridge, os tipos de aviso ficam em uma hierarquia, os canais em outra, e o aviso recebe o canal pelo construtor.

Exemplo do padrão Bridge em PHP

Vamos para o código, começando pelo lado da implementação. A interface Channel diz o que todo canal sabe fazer:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Bridge;

interface Channel
{
    public function send(string $recipient, string $text): void;
}

Cada canal concreto cuida só de como enviar. Para o exemplo ficar testável, eles apenas imprimem o que seria enviado:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Bridge;

final class EmailChannel implements Channel
{
    public function send(string $recipient, string $text): void
    {
        printf("Email to %s: %s\n", $recipient, $text);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Bridge;

final class SmsChannel implements Channel
{
    public function send(string $recipient, string $text): void
    {
        printf("SMS to %s: %s\n", $recipient, $text);
    }
}

Agora o lado da abstração. A classe Notification guarda o canal recebido no construtor. Essa propriedade é a ponte entre as duas hierarquias:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Bridge;

abstract class Notification
{
    protected Channel $channel;

    public function __construct(Channel $channel)
    {
        $this->channel = $channel;
    }

    abstract public function notify(string $recipient): void;
}

Cada tipo de aviso estende Notification e cuida só do que dizer. O envio fica com o canal:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Bridge;

final class OrderShippedNotification extends Notification
{
    private string $orderId;

    public function __construct(Channel $channel, string $orderId)
    {
        parent::__construct($channel);

        $this->orderId = $orderId;
    }

    public function notify(string $recipient): void
    {
        $this->channel->send($recipient, sprintf('Order %s has been shipped', $this->orderId));
    }
}

O aviso de pagamento recusado segue o mesmo caminho:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Bridge;

final class PaymentFailedNotification extends Notification
{
    private string $orderId;

    public function __construct(Channel $channel, string $orderId)
    {
        parent::__construct($channel);

        $this->orderId = $orderId;
    }

    public function notify(string $recipient): void
    {
        $this->channel->send($recipient, sprintf('Payment for order %s failed', $this->orderId));
    }
}

Repare que nenhuma notificação sabe se vai sair por e-mail ou por SMS, e nenhum canal sabe qual aviso está enviando.

Testando o exemplo

Os testes combinam os dois tipos de aviso com os dois canais. O último cria um canal novo (push) dentro do próprio teste, sem tocar em nenhuma classe de notificação:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Structural\Bridge;

use Growthdev\DesignPatterns\Structural\Bridge\Channel;
use Growthdev\DesignPatterns\Structural\Bridge\EmailChannel;
use Growthdev\DesignPatterns\Structural\Bridge\OrderShippedNotification;
use Growthdev\DesignPatterns\Structural\Bridge\PaymentFailedNotification;
use Growthdev\DesignPatterns\Structural\Bridge\SmsChannel;
use PHPUnit\Framework\TestCase;

final class BridgeTest extends TestCase
{
    public function testExpectOrderShippedToBeSentByEmail(): void
    {
        $notification = new OrderShippedNotification(new EmailChannel(), '1001');
        $notification->notify('dev@example.com');

        $this->expectOutputString("Email to dev@example.com: Order 1001 has been shipped\n");
    }

    public function testExpectSameNotificationToBeSentBySms(): void
    {
        $notification = new OrderShippedNotification(new SmsChannel(), '1001');
        $notification->notify('+55 11 90000-0000');

        $this->expectOutputString("SMS to +55 11 90000-0000: Order 1001 has been shipped\n");
    }

    public function testExpectPaymentFailedToWorkWithEveryChannel(): void
    {
        (new PaymentFailedNotification(new EmailChannel(), '1002'))->notify('dev@example.com');
        (new PaymentFailedNotification(new SmsChannel(), '1002'))->notify('+55 11 90000-0000');

        $this->expectOutputString(
            "Email to dev@example.com: Payment for order 1002 failed\n"
            . "SMS to +55 11 90000-0000: Payment for order 1002 failed\n"
        );
    }

    public function testExpectNewChannelWithoutChangingNotifications(): void
    {
        $push = new class implements Channel {
            /** @var string[] */
            public array $sent = [];

            public function send(string $recipient, string $text): void
            {
                $this->sent[] = sprintf('Push to %s: %s', $recipient, $text);
            }
        };

        (new OrderShippedNotification($push, '1001'))->notify('device-42');
        (new PaymentFailedNotification($push, '1002'))->notify('device-42');

        $this->assertSame(
            [
                'Push to device-42: Order 1001 has been shipped',
                'Push to device-42: Payment for order 1002 failed',
            ],
            $push->sent
        );
    }
}

É esse último teste que mostra o ganho: um canal novo é uma classe nova, e todos os avisos que já existem funcionam com ele.

Quando usar o padrão Bridge?

  • Quando uma classe cresce em duas direções independentes (tipo e canal, relatório e formato, forma e cor) e a herança começa a pedir uma classe por combinação.
  • Quando você quer escolher a implementação em tempo de execução, por exemplo o canal preferido de cada cliente.
  • Quando os dois lados precisam evoluir separados, mantidos por equipes ou pacotes diferentes.

Cuidados ao usar o Bridge

  • Se só um dos lados varia, o Bridge é estrutura demais: uma interface com as suas implementações já resolve.
  • A interface do lado da implementação precisa servir para todos os canais. Se um canal pedir um dado que os outros não usam, como o assunto do e-mail, a interface começa a ganhar parâmetros que só um deles aproveita.
  • O padrão acrescenta um nível de indireção: para saber o que um aviso faz, é preciso ler a notificação e o canal.

Bridge e Adapter: qual a diferença?

Os dois padrões usam composição para ligar um objeto a outro, e por isso o código deles se parece. A diferença está no momento e no motivo. Segundo o livro GoF, o Adapter costuma ser aplicado a sistemas que já foram projetados, enquanto o Bridge é pensado no início do projeto.

PadrãoQuando entraObjetivo
BridgeNo desenho, antes de as classes existiremDeixar abstração e implementação mudarem separadas
AdapterDepois, com as classes já prontasFazer duas interfaces incompatíveis trabalharem juntas
Diferença entre Bridge 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/Bridge 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, Composite, Decorator, 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.