← Todos los patrones

Adapter

Structural · ver código fuente en GitHub

Leer en: English · Português · Español

Category: Structural

El problema

Dos piezas de código necesitan comunicarse, pero sus interfaces no coinciden: nombres de método distintos, formas de parámetros distintas, convenciones de manejo de errores distintas. Las dos razones más comunes por las que esto sucede son (a) un lado es una API heredada o de terceros que no se puede cambiar, y (b) el lado "nuevo" se diseñó sin saber del antiguo. Reescribir el lado heredado a menudo no es una opción — puede ser un sistema mainframe, un SDK de proveedor, o simplemente código con un radio de impacto demasiado grande para tocar.

La solución

Introducir un envoltorio delgado que implemente la interfaz que el cliente espera, y traduzca cada llamada a lo que el adaptado realmente entiende.

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

Ejemplo clásico

classic/EnumerationIteratorAdapter es el ejemplo canónico en Java de este patrón: adapta el contrato Enumeration previo a Java 2 (hasMoreElements() / nextElement()) al contrato moderno Iterator (hasNext() / next()), de modo que el código escrito contra Iterator — bucles for-each, streams — puede consumir cualquier cosa que solo exponga un Enumeration. Esto es exactamente lo que resuelve la contraparte de Collections.enumeration() en el propio JDK. EnumerationIteratorAdapterTest recorre una enumeración envuelta de principio a fin y verifica que lanza NoSuchElementException una vez agotada, igual que cualquier otro Iterator.

Ejemplo aplicado: fachada sobre un sistema de cuentas de mainframe

applied/MainframeAccountGateway representa un sistema real de cuentas mainframe/COBOL: registros posicionales de ancho fijo (ACCOUNT[10] + NAME[25] + BALANCE_CENTS[10] + STATUS[1]) y una excepción comprobada ante un fallo — el tipo de interfaz que realmente se encuentra al modernizar un sistema bancario central de décadas, no una hipotética.

applied/MainframeAccountLookupAdapter expone ese gateway detrás del contrato moderno AccountLookupPort del que depende el código nuevo de microservicios. El código nuevo nunca analiza una cadena de ancho fijo ni captura una MainframeUnavailableException comprobada — el adapter absorbe ambas cosas, traduciendo la excepción comprobada heredada en una AccountLookupException no comprobada en el límite. Esta es la misma forma de poner una fachada sobre un mainframe real durante un esfuerzo de modernización: el sistema heredado no cambia, pero nada aguas abajo del adapter necesita saber que existe. MainframeAccountLookupAdapterTest cubre el análisis de registros, el registro-centinela de "cuenta desconocida", y la traducción de la excepción.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:

./gradlew :structural:adapter:jacocoTestReport

Informe en structural/adapter/build/reports/jacoco/test/html/index.html.

Lecturas adicionales

Pruebas unitarias

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 informe completo de cobertura JaCoCo →