← Todos os padrões

Adapter

Structural · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Structural

O problema

Duas partes de código precisam conversar entre si, mas suas interfaces não batem: nomes de método diferentes, formatos de parâmetro diferentes, convenções de tratamento de erro diferentes. Os dois motivos mais comuns pra isso acontecer são (a) um dos lados é uma API legada ou de terceiros que você não pode mudar, e (b) o lado "novo" foi projetado sem saber da existência do antigo. Reescrever o lado legado muitas vezes não é uma opção — pode ser um sistema de mainframe, um SDK de fornecedor, ou simplesmente código com raio de impacto grande demais pra mexer.

A solução

Introduzir um wrapper fino que implementa a interface que o cliente espera, e traduz cada chamada pro que o adaptado de fato entende.

classDiagram
    class Target {
        <<interface>>
    }
    class Adapter {
    }
    class Adaptee {
    }
    Target <|.. Adapter
    Adapter --> Adaptee : delegates to
    Client --> Target

Exemplo clássico

classic/EnumerationIteratorAdapter é o exemplo canônico em Java desse padrão: ele adapta o contrato Enumeration de antes do Java 2 (hasMoreElements() / nextElement()) pro contrato moderno Iterator (hasNext() / next()), de modo que código escrito contra Iterator — loops for-each, streams — consegue consumir qualquer coisa que só exponha um Enumeration. É exatamente isso que a contraparte de Collections.enumeration() resolve no próprio JDK. EnumerationIteratorAdapterTest percorre uma enumeração envolvida de ponta a ponta e verifica que ela lança NoSuchElementException quando esgotada, igual a qualquer outro Iterator.

Exemplo aplicado: fachada sobre um sistema de contas de mainframe

applied/MainframeAccountGateway representa um sistema real de contas mainframe/COBOL: registros posicionais de largura fixa (ACCOUNT[10] + NAME[25] + BALANCE_CENTS[10] + STATUS[1]) e uma exceção checada em caso de falha — o tipo de interface que você de fato encontra ao modernizar um sistema bancário central de décadas, não uma hipotética.

applied/MainframeAccountLookupAdapter expõe esse gateway atrás do contrato moderno AccountLookupPort do qual código novo de microsserviços depende. Código novo nunca faz parsing de uma string de largura fixa nem captura uma MainframeUnavailableException checada — o adapter absorve os dois, traduzindo a exceção checada legada numa AccountLookupException não checada na fronteira. Essa é a mesma forma de colocar uma fachada sobre um mainframe real durante um esforço de modernização: o sistema legado não muda, mas nada a jusante do adapter precisa saber que ele existe. MainframeAccountLookupAdapterTest cobre o parsing de registro, o registro-sentinela de "conta desconhecida", e a tradução da exceção.

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :structural:adapter:jacocoTestReport

Relatório em structural/adapter/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/structural/adapter/classic/EnumerationIteratorAdapterTest.java
package com.designpatterns.structural.adapter.classic;

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class EnumerationIteratorAdapterTest {

    @Test
    void walksEveryElementOfTheAdaptedEnumeration() {
        Enumeration<String> legacyEnumeration = Collections.enumeration(List.of("PIX", "TED", "BOLETO"));
        Iterator<String> iterator = new EnumerationIteratorAdapter<>(legacyEnumeration);

        assertThat(iterator).toIterable().containsExactly("PIX", "TED", "BOLETO");
    }

    @Test
    void throwsNoSuchElementExceptionOnceExhausted() {
        Enumeration<String> emptyEnumeration = Collections.enumeration(List.of());
        Iterator<String> iterator = new EnumerationIteratorAdapter<>(emptyEnumeration);

        assertThat(iterator.hasNext()).isFalse();
        assertThatThrownBy(iterator::next).isInstanceOf(NoSuchElementException.class);
    }
}
src/test/java/com/designpatterns/structural/adapter/applied/MainframeAccountLookupAdapterTest.java
package com.designpatterns.structural.adapter.applied;

import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class MainframeAccountLookupAdapterTest {

    private static final String ACCOUNT_NUMBER = "1234567";
    private static final String PADDED_ACCOUNT_NUMBER = "0001234567";

    @Test
    void parsesTheLegacyFixedWidthRecordIntoAModernSnapshot() {
        String record = PADDED_ACCOUNT_NUMBER + leftJustify("JOAO DA SILVA", 25) + "0000015000" + "A";
        MainframeAccountGateway gateway = new MainframeAccountGateway(Map.of(PADDED_ACCOUNT_NUMBER, record));
        MainframeAccountLookupAdapter adapter = new MainframeAccountLookupAdapter(gateway);

        AccountSnapshot snapshot = adapter.findByAccountNumber(ACCOUNT_NUMBER);

        assertThat(snapshot.accountNumber()).isEqualTo(PADDED_ACCOUNT_NUMBER);
        assertThat(snapshot.holderName()).isEqualTo("JOAO DA SILVA");
        assertThat(snapshot.balanceCents()).isEqualTo(15_000L);
        assertThat(snapshot.active()).isTrue();
    }

    @Test
    void returnsAnInactiveZeroBalanceSnapshotWhenTheAccountIsUnknown() {
        MainframeAccountGateway gateway = new MainframeAccountGateway(Map.of());
        MainframeAccountLookupAdapter adapter = new MainframeAccountLookupAdapter(gateway);

        AccountSnapshot snapshot = adapter.findByAccountNumber(ACCOUNT_NUMBER);

        assertThat(snapshot.balanceCents()).isZero();
        assertThat(snapshot.active()).isFalse();
    }

    @Test
    void translatesTheLegacyCheckedExceptionIntoAnUncheckedLookupException() {
        MainframeAccountGateway gateway = new MainframeAccountGateway(Map.of());
        MainframeAccountLookupAdapter adapter = new MainframeAccountLookupAdapter(gateway);

        assertThatThrownBy(() -> adapter.findByAccountNumber("9999999999"))
                .isInstanceOf(AccountLookupException.class)
                .hasCauseInstanceOf(MainframeUnavailableException.class);
    }

    private static String leftJustify(String value, int width) {
        return String.format("%-" + width + "s", value);
    }
}

Ver relatório completo de cobertura JaCoCo →