Conceito e intenção
Adapter (também chamado de Wrapper) permite que classes com interfaces incompatíveis trabalhem juntas. O padrão envolve a classe incompatível e apresenta ao cliente a interface que ele espera, traduzindo chamadas de um lado para o outro.
O problema motivador é a integração com sistemas externos. O domínio da aplicação define suas próprias interfaces (Ports, no vocabulário de Arquitetura Hexagonal). SDKs de terceiros — gateways de pagamento, APIs de SMS, serviços de armazenamento — têm interfaces próprias que não correspondem ao que o domínio espera. Sem Adapter, o código de domínio ficaria acoplado às classes do SDK externo. Trocar de fornecedor exigiria alterar o core da aplicação.
Com Adapter, o domínio fala sempre com sua interface (PaymentGateway). O Adapter vive na camada de infraestrutura e traduz as chamadas para o SDK específico. Trocar de Stripe para PayPal = criar um novo Adapter, sem tocar no domínio.
Estrutura
┌──────────────────┐
Client ──────────▶│ <<interface>> │
│ Target │
│ + request() │
└──────────────────┘
▲
┌────────┴─────────┐
│ Adapter │
│ - adaptee │
│ + request() ───┼──▶ Adaptee.specificRequest()
└──────────────────┘
┌──────────────────┐
│ Adaptee │
│ (incompatível) │
│+specificRequest()│
└──────────────────┘Participantes:
- Target — interface que o cliente conhece (Port de saída no hexagonal)
- Adaptee — classe existente com interface incompatível (SDK externo)
- Adapter — traduz chamadas de Target para Adaptee
Object Adapter vs Class Adapter
Object Adapter (mais comum, mais flexível): usa composição — o adapter contém uma instância do adaptee.
Class Adapter (menos comum, só Java/C++): usa herança múltipla — o adapter estende tanto Target quanto Adaptee. Em Java puro, isso só funciona se Target for interface e Adaptee for classe.
Object Adapter Class Adapter
───────────── ─────────────
Adapter ──HAS-A──▶ Adaptee Adapter ──IS-A──▶ Target (interface)
Adapter ──IS-A──▶ Adaptee (classe)Na prática, prefira Object Adapter: permite adaptar subclasses do Adaptee e não exige conhecimento dos internals.
Implementação Java — gateway de pagamento
// Interface do domínio (Target / Port de saída)
public interface PaymentGateway {
PaymentResult charge(Money amount, String customerId);
RefundResult refund(String paymentId, Money amount);
PaymentStatus getStatus(String paymentId);
}
// SDK Stripe — interface incompatível (Adaptee)
public class StripeClient {
public StripeCharge createCharge(long amountCents, String currency, String stripeCustomerId) { /* ... */ return null; }
public StripeRefund createRefund(String chargeId, Long amountCents) { /* ... */ return null; }
public StripeCharge retrieveCharge(String chargeId) { /* ... */ return null; }
}
// Adapter Stripe → PaymentGateway (Object Adapter via composição)
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient stripeClient;
public StripePaymentAdapter(StripeClient stripeClient) {
this.stripeClient = stripeClient;
}
@Override
public PaymentResult charge(Money amount, String customerId) {
try {
StripeCharge charge = stripeClient.createCharge(
amount.toCents(),
amount.getCurrency().getCode(),
customerId
);
return PaymentResult.success(charge.getId(), mapStatus(charge.getStatus()));
} catch (StripeException e) {
return PaymentResult.failure(e.getMessage());
}
}
@Override
public RefundResult refund(String paymentId, Money amount) {
StripeRefund refund = stripeClient.createRefund(paymentId, amount.toCents());
return new RefundResult(refund.getId(), mapRefundStatus(refund.getStatus()));
}
@Override
public PaymentStatus getStatus(String paymentId) {
StripeCharge charge = stripeClient.retrieveCharge(paymentId);
return mapStatus(charge.getStatus());
}
// Mapeamento de enums/strings do SDK para o modelo do domínio
private PaymentStatus mapStatus(String stripeStatus) {
return switch (stripeStatus) {
case "succeeded" -> PaymentStatus.APPROVED;
case "pending" -> PaymentStatus.PENDING;
case "failed" -> PaymentStatus.DECLINED;
default -> PaymentStatus.UNKNOWN;
};
}
private RefundStatus mapRefundStatus(String stripeStatus) {
return switch (stripeStatus) {
case "succeeded" -> RefundStatus.COMPLETED;
case "pending" -> RefundStatus.PENDING;
default -> RefundStatus.FAILED;
};
}
}
// SDK PagSeguro — outro adaptee com interface diferente
public class PagSeguroClient {
public TransacaoResponse criarTransacao(TransacaoRequest request) { return null; }
public EstornoBoleto estornar(String codigoTransacao) { return null; }
}
// Adapter PagSeguro → PaymentGateway
public class PagSeguroPaymentAdapter implements PaymentGateway {
private final PagSeguroClient pagSeguro;
public PagSeguroPaymentAdapter(PagSeguroClient pagSeguro) {
this.pagSeguro = pagSeguro;
}
@Override
public PaymentResult charge(Money amount, String customerId) {
TransacaoRequest req = TransacaoRequest.builder()
.valor(amount.toBigDecimal())
.compradorId(customerId)
.build();
TransacaoResponse resp = pagSeguro.criarTransacao(req);
return PaymentResult.success(resp.getCodigo(), mapStatus(resp.getStatus()));
}
@Override
public RefundResult refund(String paymentId, Money amount) {
EstornoBoleto estorno = pagSeguro.estornar(paymentId);
return new RefundResult(estorno.getId(), RefundStatus.PENDING);
}
@Override
public PaymentStatus getStatus(String paymentId) {
// PagSeguro não tem endpoint direto de consulta no exemplo
return PaymentStatus.UNKNOWN;
}
private PaymentStatus mapStatus(int status) {
return switch (status) {
case 3 -> PaymentStatus.APPROVED;
case 7 -> PaymentStatus.DECLINED;
default -> PaymentStatus.PENDING;
};
}
}
// Domínio não muda ao trocar de gateway
@Service
public class OrderPaymentService {
private final PaymentGateway gateway; // nunca conheceu StripeClient
public OrderPaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
public void processPayment(Order order) {
PaymentResult result = gateway.charge(order.getTotal(), order.getCustomerId());
if (!result.isApproved()) {
throw new PaymentDeclinedException(result.getReason());
}
order.markAsPaid(result.getPaymentId());
}
}
// Configuração (Spring Boot) — escolhe qual adapter injetar
@Configuration
public class PaymentConfig {
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "stripe")
public PaymentGateway stripeGateway(StripeClient stripeClient) {
return new StripePaymentAdapter(stripeClient);
}
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "pagseguro")
public PaymentGateway pagSeguroGateway(PagSeguroClient pagSeguroClient) {
return new PagSeguroPaymentAdapter(pagSeguroClient);
}
@Bean
@Profile("test")
public PaymentGateway fakeGateway() {
return new FakePaymentGateway(); // sem HTTP nos testes
}
}Two-Way Adapter
Um adapter que implementa interfaces dos dois lados — útil quando dois sistemas precisam integrar-se bidirecionalmente:
// Sistema A espera: DataSource
// Sistema B espera: ConnectionProvider
public class TwoWayDatabaseAdapter implements DataSource, ConnectionProvider {
private final UnderlyingDatabase db;
// Para sistema A
@Override
public Connection getConnection() {
return db.openConnection();
}
// Para sistema B
@Override
public DatabaseHandle getHandle() {
return new DatabaseHandleAdapter(db.openConnection());
}
}Implementação TypeScript
// Target — interface do domínio
interface PaymentGateway {
charge(amount: Money, customerId: string): Promise<PaymentResult>;
refund(paymentId: string, amount: Money): Promise<RefundResult>;
}
// Adaptee — SDK hipotético
class StripeSDK {
async createCharge(params: {
amount: number;
currency: string;
customer: string;
}): Promise<{ id: string; status: string }> {
// chamada HTTP real
return { id: 'ch_xxx', status: 'succeeded' };
}
async createRefund(chargeId: string): Promise<{ id: string }> {
return { id: 'rf_xxx' };
}
}
// Adapter
class StripeAdapter implements PaymentGateway {
constructor(private readonly stripe: StripeSDK) {}
async charge(amount: Money, customerId: string): Promise<PaymentResult> {
const charge = await this.stripe.createCharge({
amount: amount.toCents(),
currency: amount.currency,
customer: customerId,
});
return {
paymentId: charge.id,
status: charge.status === 'succeeded' ? 'APPROVED' : 'DECLINED',
};
}
async refund(paymentId: string): Promise<RefundResult> {
const refund = await this.stripe.createRefund(paymentId);
return { refundId: refund.id, status: 'COMPLETED' };
}
}
// Fake para testes
class FakePaymentGateway implements PaymentGateway {
async charge(): Promise<PaymentResult> {
return { paymentId: 'fake-001', status: 'APPROVED' };
}
async refund(): Promise<RefundResult> {
return { refundId: 'fake-rf-001', status: 'COMPLETED' };
}
}No mundo real
Arrays.asList() e List.of() adaptam arrays Java para a interface List — a implementação subjacente é um wrapper sobre o array, não uma ArrayList.
InputStreamReader adapta InputStream (bytes) para Reader (chars) — clássico exemplo do Java I/O:
Reader reader = new InputStreamReader(new FileInputStream("file.txt"), StandardCharsets.UTF_8);Spring Data JpaRepository adapta a interface fluente de repositório para chamadas JPA/Hibernate. Você codifica contra OrderRepository extends JpaRepository<Order, String> — o Spring gera um adapter em runtime.
SLF4J é um adapter de logging. LoggerFactory.getLogger() retorna um wrapper que delega para a implementação real (Logback, Log4j2, JUL) sem que seu código saiba qual é.
HttpServletRequestWrapper do Jakarta EE — adapter que envolve HttpServletRequest para interceptar e modificar comportamento em filtros.
Axios interceptors no frontend — permitem adaptar requests/responses globalmente, traduzindo erros HTTP para exceções do domínio sem alterar cada chamada de API.
Quando usar
- Integrar uma biblioteca ou SDK de terceiro sem acoplar o domínio a ela
- Reusar código legado com interface incompatível sem modificá-lo
- Criar uma camada de abstração entre o core da aplicação e sistemas externos (ports & adapters / hexagonal)
- Facilitar troca de fornecedor (Stripe → Mercado Pago) ou versão de API sem alterar domínio
Quando NÃO usar
- Quando a interface do SDK externo é aceitável diretamente e não há plano de trocar
- Quando o adapter se torna tão complexo que é quase um segundo domínio — considere um anti-corruption layer mais explícito
- Quando você adapta apenas para renomear métodos trivialmente — isso é boilerplate sem benefício real
Combinações com outros padrões
Adapter + Facade: Adapter cuida da incompatibilidade de interface; Facade simplifica um subsistema complexo. Um Facade pode usar múltiplos Adapters internamente para unificar SDKs distintos em uma interface única.
Adapter + Decorator: Ambos envolvem um objeto, mas com propósitos diferentes. Adapter muda a interface; Decorator adiciona comportamento mantendo a interface. Podem ser compostos: new RetryDecorator(new StripeAdapter(stripeClient)).
Adapter + Strategy: A Strategy define o algoritmo intercambiável; o Adapter adapta cada implementação externa para a interface da Strategy. Cada gateway de pagamento é uma Strategy acessível via seu Adapter.