Padrão Estrutural (GoF)

Adapter

Converte a interface de uma classe na interface que o cliente espera, permitindo que classes com interfaces incompatíveis colaborem sem modificar seu código-fonte.

Intenção

Fazer com que interfaces incompatíveis trabalhem juntas, envolvendo uma classe existente (o adaptee) em um wrapper que expõe a interface esperada pelo cliente — sem alterar o adaptee nem o cliente.

O Adapter pertence à categoria de padrões estruturais no catálogo do GoF (1994) e é um dos mais usados na integração de sistemas: sempre que você consome uma biblioteca de terceiros, um SDK externo ou uma API legada cujas interfaces não se encaixam no contrato que seu domínio define, o Adapter é a ferramenta correta.

Problema

Imagine que sua aplicação define uma interface Logger com o método log(nivel, mensagem). Você decide adotar uma biblioteca de logging de terceiros — digamos, ExternalLogger — que expõe um contrato completamente diferente: métodos separados writeInfo(msg), writeWarning(msg) e writeError(msg).

Você não pode modificar ExternalLogger (é código de terceiro, atualizado por outra equipe). Também não quer modificar toda a sua aplicação para chamar a nova API diretamente — isso cria acoplamento com o fornecedor e torna uma futura troca de biblioteca uma refatoração massiva.

A solução ingênua seria espalhar condicionais ou chamadas diretas à biblioteca por todo o código. O Adapter resolve isso de forma limpa: um único objeto intermediário traduz as chamadas.

Object Adapter vs Class Adapter

O GoF descreve duas variantes:

  • Object Adapter (composição): o adapter mantém uma referência ao adaptee e delega as chamadas a ele. É a forma idiomática em TypeScript e PHP — não depende de herança múltipla, funciona com subclasses do adaptee e é mais flexível.
  • Class Adapter (herança múltipla): o adapter herda tanto da interface-alvo quanto do adaptee. Em TypeScript e PHP, herança múltipla de classes não existe — logo, esta variante não se aplica diretamente. Em PHP, pode-se simular parcialmente com traits, mas o resultado costuma ser mais complexo sem ganho real. Prefira sempre o Object Adapter.

Nos exemplos abaixo, usamos exclusivamente o Object Adapter.

Solução

O Adapter organiza o código em quatro participantes:

  1. Target (interface-alvo): o contrato que o cliente espera. Ex.: interface Logger com log(nivel, mensagem).
  2. Adaptee (classe existente/incompatível): a classe cuja interface precisa ser adaptada. Ex.: ExternalLogger com writeInfo(), writeWarning(), writeError().
  3. Adapter: implementa a interface Target e mantém uma referência ao Adaptee. Traduz cada chamada do Target para a API do Adaptee. Ex.: ExternalLoggerAdapter implements Logger.
  4. Client: usa apenas a interface Target — nunca conhece o Adaptee diretamente.

Com o Adapter no lugar, trocar de biblioteca de logging é uma mudança de uma única classe — o resto do sistema permanece intocado.

Estrutura

   «interface» Target
  ┌───────────────────────────────┐
  │ + log(nivel, mensagem): void  │
  └───────────────────────────────┘
          ▲               ▲
          │               │ implementa
   Código cliente   ExternalLoggerAdapter
   usa o Target          (Adapter)
                    ┌──────────────────────────────┐
                    │ - adaptee: ExternalLogger     │
                    │ + log(nivel, mensagem): void  │
                    └──────────────────────────────┘
                                │ delega para
                                ▼
                        ExternalLogger
                           (Adaptee)
                    ┌──────────────────────────────┐
                    │ + writeInfo(msg): void        │
                    │ + writeWarning(msg): void     │
                    │ + writeError(msg): void       │
                    └──────────────────────────────┘


Fluxo de uma chamada:

  cliente.log("error", "Falha na conexão")
      │
      ▼ (via interface Logger)
  ExternalLoggerAdapter.log("error", "Falha na conexão")
      │
      │  switch(nivel) → chama writeError()
      ▼
  ExternalLogger.writeError("Falha na conexão")

Exemplos de código

Exemplo 1 — Adapter de logger de terceiros

O cliente conhece apenas a interface Logger. O Adapter traduz as chamadas para a API incompatível da biblioteca externa.

// ── Target — interface que o código da aplicação conhece ─────
type NivelLog = "info" | "warning" | "error";

interface Logger {
  log(nivel: NivelLog, mensagem: string): void;
}

// ── Adaptee — biblioteca de terceiro com API incompatível ─────
// (código que NÃO podemos modificar)
class ExternalLogger {
  writeInfo(msg: string): void {
    console.log(`[INFO]    ${msg}`);
  }
  writeWarning(msg: string): void {
    console.warn(`[WARNING] ${msg}`);
  }
  writeError(msg: string): void {
    console.error(`[ERROR]   ${msg}`);
  }
}

// ── Adapter (Object Adapter via composição) ───────────────────
class ExternalLoggerAdapter implements Logger {
  // Mantém referência ao adaptee — sem herdar dele.
  constructor(private readonly adaptee: ExternalLogger) {}

  log(nivel: NivelLog, mensagem: string): void {
    switch (nivel) {
      case "info":    this.adaptee.writeInfo(mensagem);    break;
      case "warning": this.adaptee.writeWarning(mensagem); break;
      case "error":   this.adaptee.writeError(mensagem);   break;
    }
  }
}

// ── Código cliente — usa apenas a interface Logger ────────────
function processarPedido(logger: Logger, pedidoId: string): void {
  logger.log("info",    `Iniciando processamento do pedido ${pedidoId}`);
  logger.log("warning", `Estoque baixo para o pedido ${pedidoId}`);
  logger.log("error",   `Falha no pagamento do pedido ${pedidoId}`);
}

// Montagem — o cliente nunca sabe que existe um ExternalLogger:
const logger = new ExternalLoggerAdapter(new ExternalLogger());
processarPedido(logger, "PED-001");
// [INFO]    Iniciando processamento do pedido PED-001
// [WARNING] Estoque baixo para o pedido PED-001
// [ERROR]   Falha no pagamento do pedido PED-001

Exemplo 2 — Adapter de gateway de pagamento

Um caso muito comum na prática: sua aplicação define um contrato de pagamento próprio, e cada provider (Stripe, PagSeguro, Mercado Pago) tem sua própria SDK com API diferente. Um Adapter por provider mantém o domínio isolado.

// ── Target — contrato de pagamento do domínio da aplicação ───
interface GatewayPagamento {
  cobrar(valorCentavos: number, descricao: string): Promise<string>;
}

// ── Adaptee A — SDK do Stripe (interface incompatível) ───────
class StripeSdk {
  async createCharge(amount: number, currency: string, desc: string): Promise<{ id: string }> {
    // Simulação da chamada real ao Stripe
    return { id: `stripe_ch_${Date.now()}` };
  }
}

// ── Adaptee B — SDK do PagSeguro (interface diferente) ───────
class PagSeguroSdk {
  async realizarCobranca(params: {
    valor: number;
    moeda: string;
    referencia: string;
  }): Promise<{ transacaoId: string }> {
    return { transacaoId: `pag_${Date.now()}` };
  }
}

// ── Adapter A ─────────────────────────────────────────────────
class StripeAdapter implements GatewayPagamento {
  constructor(private readonly sdk: StripeSdk) {}

  async cobrar(valorCentavos: number, descricao: string): Promise<string> {
    const resultado = await this.sdk.createCharge(
      valorCentavos,
      "BRL",
      descricao
    );
    return resultado.id;
  }
}

// ── Adapter B ─────────────────────────────────────────────────
class PagSeguroAdapter implements GatewayPagamento {
  constructor(private readonly sdk: PagSeguroSdk) {}

  async cobrar(valorCentavos: number, descricao: string): Promise<string> {
    const resultado = await this.sdk.realizarCobranca({
      valor: valorCentavos / 100, // PagSeguro usa reais, não centavos
      moeda: "BRL",
      referencia: descricao,
    });
    return resultado.transacaoId;
  }
}

// ── Código cliente — desacoplado do provider ──────────────────
async function processarPagamento(
  gateway: GatewayPagamento,
  valorCentavos: number
): Promise<void> {
  const transacaoId = await gateway.cobrar(valorCentavos, "Assinatura mensal");
  console.log(`Pagamento aprovado. Transação: ${transacaoId}`);
}

// Troca de provider = trocar apenas a linha de montagem:
const gateway = new StripeAdapter(new StripeSdk());
await processarPagamento(gateway, 4990); // R$49,90

Quando usar

  • Integrar uma biblioteca ou SDK de terceiros cuja interface não corresponde ao contrato do seu domínio — e você não pode modificar a biblioteca.
  • Isolar o domínio de providers externos: gateways de pagamento, serviços de e-mail, provedores de SMS, APIs de geolocalização. O Adapter cria uma barreira anti-corrupção: o domínio evolui sem acoplamento ao provider.
  • Reutilizar classes legadas com interfaces incompatíveis com o código novo — sem reescrevê-las.
  • Suportar múltiplas implementações de um serviço de forma transparente para o cliente (vide o exemplo com Stripe e PagSeguro).

Quando evitar

  • Quando você controla ambas as interfaces: se você pode modificar o adaptee para que ele implemente diretamente a interface-alvo, não há necessidade de um Adapter — modifique a fonte.
  • Quando a diferença de interface é trivial: renomear um parâmetro não justifica criar uma camada de indireção inteira.
  • Como substituto para refatoração: Adapters sobre código interno ruim são um paliativo. Em código próprio mal estruturado, a solução é refatorar, não adaptar.

Prós e contras

Prós

  • Princípio da Responsabilidade Única: a conversão de interface fica em uma classe dedicada.
  • Princípio Aberto/Fechado: novos providers exigem apenas um novo Adapter, sem tocar no código existente.
  • Isola o código de negócio de SDKs e APIs de terceiros — facilita testes (basta injetar um fake que implementa a interface-alvo).
  • Permite trocar a implementação subjacente sem alterar o cliente.

Contras

  • Adiciona uma camada de indireção — aumenta o número de classes e pode tornar o fluxo menos óbvio na primeira leitura.
  • Se a diferença entre as interfaces for muito grande (paradigmas completamente distintos), o Adapter pode tornar-se complexo e difícil de manter.
  • Adapters sobre Adapters (cadeia longa) são um sinal de que o design precisa ser revisado.

Armadilhas comuns

1. Confundir Adapter com Facade

Adapter e Facade são superficialmente parecidos — ambos envolvem código existente. A diferença-chave: o Adapter converte uma interface incompatível em outra esperada pelo cliente (foco em compatibilidade de interface). A Facade simplifica e unifica um subsistema complexo em uma interface de alto nível (foco em simplificação de uso). Um Adapter pode adaptar uma única classe; uma Facade costuma orquestrar múltiplas classes.

2. Vazar a interface do adaptee para o cliente

Atenção: o objetivo do Adapter é que o cliente nunca referencie o adaptee diretamente. Se o código cliente importa ou conhece ExternalLogger, o isolamento foi comprometido. O Adapter deve ser a única fronteira — injete-o via interface no cliente e mantenha o adaptee encapsulado.

3. Adapter com lógica de negócio

O Adapter deve apenas traduzir chamadas — não deve conter lógica de negócio. Se você se vê adicionando regras de validação ou transformações complexas de dados dentro do Adapter, extraia essas responsabilidades para um serviço separado. O Adapter que faz demais viola o Princípio da Responsabilidade Única e se torna difícil de testar.

4. Não criar testes com o adaptee real

Testes unitários do Adapter devem usar um mock do adaptee (para isolar a lógica de tradução). Testes de integração com o adaptee real (ex.: chamada de fato ao Stripe em modo sandbox) são necessários, mas separados. Não misture os dois níveis no mesmo conjunto de testes.

Padrões relacionados

O Adapter é frequentemente comparado e combinado com outros padrões estruturais:

O Decorator mantém a mesma interface que o objeto decorado — seu objetivo é adicionar responsabilidades, não mudar o contrato. O Adapter, ao contrário, tem exatamente o objetivo de mudar a interface. A Facade simplifica um subsistema; não adapta uma interface incompatível — mas ambas criam um ponto de entrada único para código externo. O Proxy também envolve o objeto original, mas mantém a mesma interface e adiciona controle de acesso, cache ou lazy initialization — não muda o contrato. O Bridge separa uma abstração de sua implementação desde o design; o Adapter reconcilia interfaces incompatíveis que existem de forma independente.