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
- Si usted controla ambos lados de la interfaz y solo son inconsistentes por accidente, corrija la inconsistencia en vez de adaptarse a su alrededor — un adapter debería tender un puente entre dos cosas que cada una tiene una razón legítima para ser como es.
- No deje que los adapters acumulen lógica de negocio. El trabajo de un adapter es traducción, no validación ni toma de decisiones — si empieza a hacer cualquiera de las dos, esa lógica pertenece una capa más arriba.
- Si está adaptando la misma interfaz en muchos lugares no relacionados, considere si una verdadera capa anticorrupción (un pequeño módulo interno, no solo una clase) encaja mejor que esparcir adapters por toda la base de código.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — el Capítulo 4 formaliza Adapter (tanto la variante de objeto como la de clase).
- Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall. — introduce el Principio Abierto-Cerrado; un adapter es una aplicación directa de él, extendiendo la compatibilidad con una interfaz nueva sin modificar ni al cliente ni al adaptado.
- Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley. — introduce la Anti-Corruption Layer, la generalización a nivel de módulo de lo que un único Adapter hace a nivel de clase; referenciada directamente en "Cuándo no usarlo" arriba.
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);
}
}