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:
-
Target (interface-alvo): o contrato que o cliente espera.
Ex.: interface
Loggercomlog(nivel, mensagem). -
Adaptee (classe existente/incompatível): a classe cuja
interface precisa ser adaptada. Ex.:
ExternalLoggercomwriteInfo(),writeWarning(),writeError(). -
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. - 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
<?php
// ── Target — interface que o código da aplicação conhece ─────
interface Logger
{
public function log(string $nivel, string $mensagem): void;
}
// ── Adaptee — biblioteca de terceiro com API incompatível ─────
// (código que NÃO podemos modificar)
class ExternalLogger
{
public function writeInfo(string $msg): void
{
echo "[INFO] {$msg}" . PHP_EOL;
}
public function writeWarning(string $msg): void
{
echo "[WARNING] {$msg}" . PHP_EOL;
}
public function writeError(string $msg): void
{
echo "[ERROR] {$msg}" . PHP_EOL;
}
}
// ── Adapter (Object Adapter via composição) ───────────────────
class ExternalLoggerAdapter implements Logger
{
// Mantém referência ao adaptee — sem herdar dele.
public function __construct(
private readonly ExternalLogger $adaptee
) {}
public function log(string $nivel, string $mensagem): void
{
match ($nivel) {
'info' => $this->adaptee->writeInfo($mensagem),
'warning' => $this->adaptee->writeWarning($mensagem),
'error' => $this->adaptee->writeError($mensagem),
default => $this->adaptee->writeInfo($mensagem),
};
}
}
// ── Código cliente — usa apenas a interface Logger ────────────
function processarPedido(Logger $logger, string $pedidoId): 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:
$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
<?php
// ── Target — contrato de pagamento do domínio da aplicação ───
interface GatewayPagamento
{
public function cobrar(int $valorCentavos, string $descricao): string;
}
// ── Adaptee A — SDK do Stripe (interface incompatível) ───────
class StripeSdk
{
public function createCharge(
int $amount,
string $currency,
string $desc
): array {
// Simulação da chamada real ao Stripe
return ['id' => 'stripe_ch_' . time()];
}
}
// ── Adaptee B — SDK do PagSeguro (interface diferente) ───────
class PagSeguroSdk
{
public function realizarCobranca(array $params): array
{
return ['transacaoId' => 'pag_' . time()];
}
}
// ── Adapter A ─────────────────────────────────────────────────
class StripeAdapter implements GatewayPagamento
{
public function __construct(private readonly StripeSdk $sdk) {}
public function cobrar(int $valorCentavos, string $descricao): string
{
$resultado = $this->sdk->createCharge($valorCentavos, 'BRL', $descricao);
return $resultado['id'];
}
}
// ── Adapter B ─────────────────────────────────────────────────
class PagSeguroAdapter implements GatewayPagamento
{
public function __construct(private readonly PagSeguroSdk $sdk) {}
public function cobrar(int $valorCentavos, string $descricao): string
{
// PagSeguro usa reais, não centavos
$resultado = $this->sdk->realizarCobranca([
'valor' => $valorCentavos / 100,
'moeda' => 'BRL',
'referencia' => $descricao,
]);
return $resultado['transacaoId'];
}
}
// ── Código cliente — desacoplado do provider ──────────────────
function processarPagamento(GatewayPagamento $gateway, int $valorCentavos): void
{
$transacaoId = $gateway->cobrar($valorCentavos, 'Assinatura mensal');
echo "Pagamento aprovado. Transação: {$transacaoId}" . PHP_EOL;
}
// Troca de provider = trocar apenas a linha de montagem:
$gateway = new StripeAdapter(new StripeSdk());
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.