O adaptador de tomada de viagem
Seu notebook tem plug brasileiro; a tomada do hotel em Londres tem três pinos retangulares. Você não refaz o cabo do notebook nem a parede do hotel — encaixa um adaptador de viagem entre os dois. O notebook continua 'achando' que está numa tomada normal, a parede continua recebendo o plug que espera, e a energia passa. O Adapter faz exatamente isso com interfaces de código: ninguém muda, alguém traduz.
O problema
Você encontrou a classe perfeita — um SDK de e-mail, um client de pagamento, uma lib de datas — mas a assinatura dela não bate com a interface que o seu domínio já usa. O compilador não aceita, e o encaixe direto não existe.
Reescrever a biblioteca não é opção (é de terceiros); mudar todos os pontos do seu código que dependem da interface atual é caro e arriscado.
A raiz do problema é que interfaces são contratos rígidos por design: é justamente essa rigidez que dá segurança de tipos. O preço aparece na fronteira entre códigos que evoluíram separados — seu domínio modelou 'enviar(mensagem)' pensando no negócio, o SDK expôs 'dispatch(payload)' pensando na generalidade dele. Nenhum dos dois está errado; eles só nunca se conheceram.
A tentação ingênua é 'deixar o SDK vazar': fazer o domínio inteiro passar a falar dispatch/payload. Funciona hoje, mas acopla todas as camadas ao vocabulário de um fornecedor — e quando o fornecedor mudar (ou você trocar de fornecedor), a mudança se espalha por todo lugar. O Adapter inverte isso: o vocabulário do domínio manda, e a tradução fica concentrada em um único arquivo.
Vale distinguir do Facade e do Decorator, que também 'embrulham' objetos: o Facade simplifica um subsistema inteiro criando uma interface nova; o Decorator mantém a mesma interface e adiciona comportamento. O Adapter não simplifica nem adiciona — converte uma interface existente em outra existente, sem mudar a semântica.
- Cliente
- chama enviar()Interface Alvo
- implementaAdaptador
- traduz p/ dispatch()SDK incompatível
Ligue o adaptador e veja a tradução
Mexa nos controles: a teoria acima vira comportamento observável.
Cliente
enviar("Olá")SDK externo
dispatch({ body: "Olá" })Navegue pelas camadas
Camada 1/4Cliente
use ← →Camada 1
Cliente
usa o contrato do domínio, sem saber quem o cumpre
É qualquer código seu que precisa do serviço — um caso de uso, um controller. Ele depende apenas da interface alvo e recebe a implementação de fora (injeção). Nunca importa nem menciona o SDK.
No código
const n: Notificador = new EmailAdapter(new SdkEmail());
n.enviar("Olá");Se violar: se o cliente importar o SDK direto, o acoplamento se espalha: trocar de fornecedor passa a exigir mudanças em todos os pontos de uso.
Estrutura
Código
// Interface que o domínio espera
interface Notificador {
enviar(mensagem: string): void;
}
// SDK externo com outra assinatura
class SdkEmail {
dispatch(payload: { body: string }) { /* ... */ }
}
// Adapter
class EmailAdapter implements Notificador {
constructor(private sdk: SdkEmail) {}
enviar(mensagem: string) {
this.sdk.dispatch({ body: mensagem }); // traduz
}
}
const n: Notificador = new EmailAdapter(new SdkEmail());
n.enviar("Olá");interface Notificador {
void enviar(String mensagem);
}
class SdkEmail {
void dispatch(String body) { /* ... */ }
}
class EmailAdapter implements Notificador {
private final SdkEmail sdk;
EmailAdapter(SdkEmail sdk) { this.sdk = sdk; }
public void enviar(String mensagem) {
sdk.dispatch(mensagem); // traduz
}
}Casos de uso
Gateway de pagamento múltiplo
Cenário
Um e-commerce precisa aceitar Stripe, PagSeguro e Pix — cada um com SDK, nomes e formatos de resposta completamente diferentes.
Como o padrão entra
O domínio define uma interface única (cobrar, estornar, consultar) e cada provedor ganha seu adaptador. O checkout não sabe qual gateway está ativo; adicionar um quarto provedor é escrever mais um adaptador, sem tocar no fluxo.
Trade-off: A interface comum tende ao mínimo denominador: recursos exclusivos de um provedor (parcelamento especial, antifraude próprio) ficam difíceis de expor sem vazar detalhes.
Migração de SDK sem big-bang
Cenário
O fornecedor descontinuou a v1 do SDK e a v2 mudou todas as assinaturas. Há dezenas de pontos de uso no sistema e ninguém quer uma migração de semanas em uma branch gigante.
Como o padrão entra
Escreve-se um adaptador que expõe a interface antiga por cima do SDK novo. O sistema inteiro migra em um commit (troca a implementação injetada) e os pontos de uso são modernizados aos poucos, com o adaptador encolhendo até sumir.
Trade-off: O adaptador é código temporário que vira permanente se ninguém terminar a migração — precisa de dono e prazo, senão vira camada fóssil.
Camada anticorrupção contra API legada
Cenário
Um sistema novo precisa consumir um ERP legado cheio de nomes crípticos, códigos mágicos e campos reaproveitados (o clássico 'campo observação que guarda três coisas').
Como o padrão entra
Adaptadores formam a fronteira anticorrupção (DDD): traduzem os conceitos tortos do legado para o modelo limpo do sistema novo. A sujeira fica confinada na borda; o domínio novo nunca vê um código mágico.
Trade-off: A tradução tem custo real de manutenção — cada mudança do legado exige atualizar o adaptador — e mapeamentos semânticos errados na borda corrompem silenciosamente o modelo novo.
- 1
Fixar a interface alvo
Escreva o contrato no vocabulário do domínio (`enviar(mensagem)`), sem nenhum tipo do SDK. Esse é o lado estável.
- 2
Embrulhar o incompatível
O adaptador implementa o alvo e recebe o SDK por construtor. Por dentro, traduz nomes, formatos e erros.
- 3
Injetar no cliente
Quem usa depende só da interface alvo. Trocar de fornecedor vira trocar o adaptador injetado — zero diff nos casos de uso.
- 4
Manter o adaptador fino
Só tradução. Regra de negócio aqui some quando o fornecedor muda — e a decisão precisa viver no domínio.
O Adapter que vaza o SDK
Compila, roda, passa no code review. E quebra depois.
Existe uma classe chamada Adapter e uma interface alvo — mas a assinatura carrega SdkPayload e SdkReceipt. O tradutor virou repasse; o domínio continua falando a língua do fornecedor.
// "Temos Adapter"... e os tipos do SDK vazaram pela interface alvo.
import type { SdkPayload, SdkReceipt } from "sdk-email";
interface Notificador {
enviar(payload: SdkPayload): SdkReceipt; // <- dominio fala SDK
}
class EmailAdapter implements Notificador {
constructor(private sdk: SdkEmail) {}
enviar(payload: SdkPayload) {
return this.sdk.dispatch(payload); // repasse, nao traducao
}
}
// Todo cliente importa SdkPayload. Trocar de fornecedor
// espalha mudanca pelo sistema inteiro — o adaptador nao isolou nada.O que quebra
- Ao trocar de fornecedorTodos os clientes que montam
SdkPayloadprecisam mudar — o isolamento era só de nome. - No importArquivos de domínio importam tipos do SDK; a fronteira anticorrupção não existe.
- No testeO falso precisa fabricar objetos do fornecedor real, em vez de mensagens do domínio.
A saída: A interface alvo usa apenas tipos seus: enviar(mensagem: string): void. Conversão completa — ida e volta, inclusive de erros — é o trabalho do adaptador.
Armadilhas
Deixar o tipo do SDK vazar pela interface
Se enviar() recebe ou retorna um tipo do fornecedor (SdkPayload, StripeCharge), o adaptador não isolou nada: todo cliente continua acoplado ao SDK. A interface alvo deve usar apenas tipos do seu domínio — a conversão completa é o trabalho do adaptador.
Regra de negócio escondida no adaptador
Um 'if valor > 10000, dividir em duas cobranças' dentro do adaptador é uma decisão de negócio morando em uma camada de tradução. Quando o fornecedor for trocado, a regra some junto. Adaptador traduz; quem decide é o domínio.
Adaptador sobre adaptador sobre adaptador
Cada camada de adaptação extra é uma indireção que o leitor precisa atravessar para descobrir o que realmente acontece. Se você controla os dois lados, alinhe as interfaces e apague o adaptador — o padrão existe para fronteiras que você não controla.
Quando usar × quando evitar
Use quando
3- Integrar uma biblioteca de terceiros com contrato diferente do seu domínio.
- Isolar o resto do sistema de detalhes de um SDK/API externa (camada anticorrupção).
- Reaproveitar uma classe existente cuja interface você não controla.
Evite quando
2- Você controla ambos os lados e pode simplesmente alinhar as interfaces.
- Muitas camadas de adaptação empilhadas viram indireção que ninguém entende.
Testar o que ficou
3 armadilhas de Adapter viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Wrappers de driverA camada fina que traduz a API de um serviço externo para a interface que o seu domínio espera.Array.from(arguments)Converte um objeto parecido com array para um array de verdade, sem mudar a origem.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Traduz a interface de la para a que voce espera.
const repo = { salvar: (p) => sdkExterno.persist(paraDto(p)) };O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Uma tradução a manter sempre que qualquer um dos dois lados muda
- Erros do sistema externo precisam ser traduzidos também, senão o acoplamento volta pelo
catch
Não vale se: você controla os dois lados. Aí é mais barato mudar um deles do que manter um tradutor para sempre.