Design Patterns: Adapter (Parte 4)

·5 min de leitura

Contexto

Se a Parte 3 foi sobre criar objetos de forma organizada, agora entramos em padrões estruturais, padrões que ajudam a encaixar peças sem precisar "quebrar" o resto do sistema. O Adapter é provavelmente o mais comum do dia a dia, porque ele aparece sempre que você integra com legado, SDK de terceiros ou qualquer coisa que não foi desenhada com o seu domínio em mente.

A situação típica é: seu código fala uma língua (uma interface interna estável) e o componente externo fala outra (uma API que você não controla). Em vez de contaminar seu domínio com detalhes do fornecedor, você coloca um tradutor no meio. O domínio continua falando do jeito dele; o Adapter faz a ponte.

A intenção

A intenção do Adapter é simples e extremamente pragmática: permitir que duas interfaces incompatíveis trabalhem juntas sem você reescrever o cliente nem pedir para o fornecedor mudar o SDK. O Adapter implementa a interface que o seu sistema espera e, por baixo, delega para o componente externo, traduzindo nomes, formatos e erros.

Exemplo na prática

Vamos montar o cenário passo a passo. Primeiro o problema, depois a solução.

1. O fornecedor que você não controla

Você contratou um serviço de SMS. O SDK dele expõe este método:

<?php

declare(strict_types=1);

final class SmsVendorClient
{
    public function dispatch(array $payload): int
    {
        // chamada HTTP para a API do fornecedor
        return 200; // status code
    }
}

Repare: ele recebe um array genérico e devolve um int. A assinatura não diz nada sobre "para quem" ou "qual mensagem". Para usar, você precisa conhecer a estrutura exata do array:

$client = new SmsVendorClient();

$client->dispatch([
    'destination' => '+55 11 99999-9999',
    'text'        => 'Pedido confirmado!',
    'meta'        => ['source' => 'checkout'],
]);

2. O problema: acoplar o domínio ao fornecedor

Se o CheckoutService usar o SmsVendorClient diretamente, ele fica amarrado a essa estrutura:

final class CheckoutService
{
    public function __construct(private readonly SmsVendorClient $client) {}

    public function confirmOrder(string $phone, string $orderId): void
    {
        // regras de negócio...

        $this->client->dispatch([
            'destination' => $phone,
            'text'        => "Pedido {$orderId} confirmado.",
            'meta'        => ['source' => 'checkout'],
        ]);
    }
}

Qual o risco? Se o fornecedor mudar o formato do payload (ou você trocar de fornecedor), toda classe que chama dispatch precisa ser alterada. O detalhe externo "vazou" para dentro do domínio.

3. A solução: uma interface interna + Adapter

Primeiro, defina o contrato que faz sentido para o seu domínio, sem pensar no fornecedor:

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

Agora crie o Adapter, uma classe que implementa a sua interface e delega para o fornecedor, traduzindo os dados:

final class SmsVendorNotifierAdapter implements Notifier
{
    public function __construct(private readonly SmsVendorClient $client) {}

    public function send(string $to, string $message): void
    {
        $payload = [
            'destination' => $to,
            'text'        => $message,
            'meta'        => ['source' => 'checkout'],
        ];

        $status = $this->client->dispatch($payload);

        if ($status >= 400) {
            throw new RuntimeException('Falha ao enviar SMS via vendor.');
        }
    }
}

Toda a "tradução" fica isolada aqui: montagem do array, chamada ao SDK e tratamento de erro. Nenhum outro lugar do sistema precisa saber como o fornecedor funciona.

4. O domínio agora depende só da interface

O CheckoutService passa a receber Notifier — a interface que você definiu:

final class CheckoutService
{
    public function __construct(private readonly Notifier $notifier) {}

    public function confirmOrder(string $phone, string $orderId): void
    {
        // regras de negócio...

        $this->notifier->send($phone, "Pedido {$orderId} confirmado.");
    }
}

Compare com a versão do passo 2: não existe mais array mágico, nem dependência de SmsVendorClient. O domínio ficou limpo.

5. Montagem (composition root)

Na raiz da aplicação, você conecta as peças:

$vendorClient = new SmsVendorClient();
$notifier     = new SmsVendorNotifierAdapter($vendorClient);
$checkout     = new CheckoutService($notifier);

$checkout->confirmOrder('+55 11 99999-9999', 'A-123');

O que você ganha

  • Trocar de fornecedor não altera o domínio, basta criar outro Adapter (ex: TwilioNotifierAdapter).
  • Tratamento de erro, retry e timeout ficam centralizados dentro do Adapter.
  • O domínio nunca precisa conhecer payload, headers ou qualquer detalhe de integração.

Adapter costuma aparecer quando:

  1. há integração com legado ou terceiros; e
  2. você quer isolar mudanças externas atrás de uma interface interna (a mesma ideia que aparece em Ports and Adapters).

Adapter vs Facade

Os dois "escondem" complexidade, mas com intenções diferentes. Adapter existe para resolver incompatibilidade de interfaces. Já Facade existe para simplificar o uso de um subsistema grande oferecendo uma interface mais fácil (veremos na Parte 6).

Conclusão

Adapter é um tradutor. Ele reduz o acoplamento do seu núcleo com detalhes externos — e isso paga muito bem em manutenção, especialmente quando o sistema começa a crescer e integrações viram parte relevante do produto.

Referências

  • Design Patterns (GoF).
  • Martin Fowler – Patterns of Enterprise Application Architecture
  • Refactoring Guru – Adapter: https://refactoring.guru/design-patterns/adapter

Links