Design Patterns

Adapter

Converte a interface de uma classe para outra que o cliente espera

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.