Programación · Patrones de diseño
Patrón Adapter explicado con ejemplos
Respuesta corta
Adapter es un patrón de diseño estructural que convierte la interfaz de una clase existente en la interfaz que espera el código cliente. El adaptador envuelve el objeto incompatible y traduce cada llamada, por ejemplo convirtiendo charge(amountCents) en un makePayment("12.50") heredado. Te permite reutilizar un componente de terceros o heredado sin cambiarlo y sin extender su API incómoda por tu código. También se conoce como Wrapper.
Intención
Convertir la interfaz de una clase en otra interfaz que los clientes esperan. El patrón Adapter permite que trabajen juntas clases que de otro modo no podrían por tener interfaces incompatibles. Es uno de los siete patrones estructurales del libro de la Banda de los Cuatro (GoF).
El problema que resuelve
El código de tu tienda trabaja con una pequeña interfaz PaymentGateway: charge(amountCents, currency) devuelve un resultado con un indicador ok y un identificador de transacción. Ahora el negocio firma un contrato con un proveedor de pagos cuyo SDK es completamente distinto: los importes son cadenas decimales, la moneda forma parte de un objeto de petición y los errores llegan como códigos de estado numéricos.
No puedes cambiar el SDK y no quieres reescribir todos los llamadores para que usen la nueva forma. Si el estilo del SDK se filtra al pago, los reembolsos y los informes, cambiar de proveedor más adelante será caro. Un adaptador mantiene la traducción en una sola clase.
Estructura y participantes
| Participante | Responsabilidad | En el ejemplo |
|---|---|---|
| Target | La interfaz que espera el cliente. | PaymentGateway |
| Adaptee | La clase existente con una interfaz incompatible. | LegacyPayClient |
| Adapter | Implementa Target y traduce las llamadas al Adaptee. | LegacyPayAdapter |
| Client | Trabaja solo con la interfaz Target. | checkout |
Implementación en TypeScript
// Target: what our code expects.
interface PaymentGateway {
charge(amountCents: number, currency: string): { ok: boolean; id: string };
}
// Adaptee: a third-party SDK we cannot change.
class LegacyPayClient {
makePayment(req: { amount: string; curr: string }): { status: number; ref: string } {
return { status: req.curr === "USD" ? 200 : 422, ref: `TX-${req.amount}` };
}
}
// Adapter: implements the Target by translating to the Adaptee.
class LegacyPayAdapter implements PaymentGateway {
constructor(private client: LegacyPayClient) {}
charge(amountCents: number, currency: string) {
const res = this.client.makePayment({
amount: (amountCents / 100).toFixed(2),
curr: currency.toUpperCase(),
});
return { ok: res.status === 200, id: res.ref };
}
}
// Client code knows nothing about LegacyPayClient.
function checkout(gateway: PaymentGateway, totalCents: number): string {
const result = gateway.charge(totalCents, "usd");
return result.ok ? `paid ${result.id}` : "payment failed";
}
console.log(checkout(new LegacyPayAdapter(new LegacyPayClient()), 1250)); // paid TX-12.50Cambiar más adelante a otro proveedor significa escribir un adaptador más. checkout y todos los demás llamadores siguen igual, y las pruebas pueden pasar un PaymentGateway simulado.
Adapter de objeto frente a adapter de clase
El libro GoF describe dos formas:
| Forma | Cómo funciona | Notas |
|---|---|---|
| Adapter de objeto | Mantiene una referencia al adaptado y delega en él (composición). | Funciona con el adaptado y todas sus subclases. La opción habitual en TypeScript, Java y C#. |
| Adapter de clase | Hereda del adaptado e implementa la interfaz de destino. | Necesita heredar del adaptado, por lo que adapta solo esa clase. Común en C++ con herencia múltiple. |
En TypeScript y JavaScript, adaptar una función suele bastar. Node.js incluye un pequeño adaptador de este tipo: util.promisify() convierte una función al estilo callback en otra que devuelve una promesa.
import { promisify } from "node:util";
// Adaptee: old callback style (error first, then result).
function readConfig(name: string, cb: (err: Error | null, value?: string) => void): void {
setTimeout(() => cb(null, `config for ${name}`), 10);
}
// Adapted: the same function, now with a promise-based interface.
const readConfigAsync = promisify(readConfig);
console.log(await readConfigAsync("app")); // config for appCuándo usarlo y cuándo no
Usa el patrón Adapter cuando:
- Quieras usar una clase existente, pero su interfaz no coincida con la que necesita tu código.
- Integres bibliotecas de terceros o sistemas heredados y quieras mantener su API en el borde de tu programa.
- Quieras varias implementaciones distintas, como dos proveedores de pago, detrás de una sola interfaz.
Evítalo cuando:
- Seas dueño de ambos lados. Cambiar una de las interfaces suele ser más simple que añadir una capa de traducción.
- El adaptador tuviera que simular grandes partes de la interfaz de destino que el adaptado no puede soportar realmente. Eso oculta una incompatibilidad real en lugar de resolverla.
Usos en el mundo real
- Node.js:
util.promisify()adapta funciones basadas en callbacks a promesas. - Java:
Arrays.asList()permite usar un arreglo a través de la interfazList, eInputStreamReaderpermite leer unInputStreambasado en bytes como unReaderbasado en caracteres. - Las bibliotecas de bases de datos y los ORM usan un adaptador o controlador por base de datos, de modo que la misma API de consultas funciona con PostgreSQL, MySQL o SQLite.
- El código de frontend suele envolver las API del navegador o los SDK de analítica detrás de una pequeña interfaz propia del proyecto por la misma razón.
Patrones relacionados
| Patrón | En qué se diferencia de Adapter |
|---|---|
| Decorator | Mantiene la misma interfaz y añade comportamiento. Adapter cambia la interfaz y no añade comportamiento nuevo. |
| Proxy | Mantiene la misma interfaz y controla el acceso al objeto (carga perezosa, caché, permisos). |
| Facade | Define una interfaz nueva y más simple para todo un subsistema de muchas clases. Adapter hace que una interfaz existente coincida con otra existente. |
| Bridge | Se diseña desde el principio para que una abstracción y su implementación puedan variar por separado. Adapter suele añadirse más tarde para que encajen clases existentes. |
Preguntas frecuentes
¿Cuál es la diferencia entre el patrón Adapter y el patrón Facade?
Un adaptador hace que una clase existente coincida con una interfaz que el cliente ya espera. Una fachada crea una interfaz nueva y más simple delante de un subsistema complejo de muchas clases.
¿Cuál es la diferencia entre el patrón Adapter y el patrón Decorator?
Ambos envuelven un objeto. Un decorador mantiene la misma interfaz y añade comportamiento; un adaptador cambia la interfaz para que el objeto envuelto pueda usarse donde de otro modo no podría.
¿Adapter es lo mismo que un wrapper?
Wrapper es otro nombre de Adapter en el libro GoF. En el habla cotidiana, «wrapper» también se usa para decoradores y proxies, así que la intención te dice a qué patrón se refiere.
¿Un adaptador puede adaptar varias clases?
Un adapter de objeto puede trabajar con un adaptado y todas sus subclases. Adaptar clases sin relación suele significar un adaptador por clase, todos implementando la misma interfaz de destino.