Adapter y Facade
En resumen: Adapter traduce una interfaz ajena a la que espera tu código, y Facade esconde varios subsistemas (almacén, pago, factura, correo) detrás de un solo método.
Problema
El nuevo proveedor no encaja
La tienda paga a través de su propia interfaz: Charge recibe el importe en euros y dice si el pago salió bien. La tienda no sabe quién está detrás:
interface IPaymentGateway{ bool Charge(decimal amount);}
class Shop{ private readonly IPaymentGateway gateway;
public Shop(IPaymentGateway gateway) { this.gateway = gateway; }
public void Pay(string order, decimal amount) { if (gateway.Charge(amount)) { Console.WriteLine(order + ": pagado"); } else { Console.WriteLine(order + ": rechazado"); } }}La empresa cambia a FastPay, que cobra menos comisiones. FastPay entrega una biblioteca ya hecha:
// La biblioteca de FastPay: código ajeno que no puedes cambiar.class FastPayClient{ public string MakePayment(string currency, long amountInCents, Dictionary<string, string> options) { Console.WriteLine($"FastPay: {amountInCents} {currency}"); if (amountInCents > 100000) { return "DECLINED"; } return "APPROVED"; }}Hace el mismo trabajo, con otra forma. El método se llama MakePayment, pide una moneda, un importe en céntimos y opciones, y devuelve el texto "APPROVED" o "DECLINED" en lugar de sí o no. Si se la pasas tal cual a la tienda, no encaja:
var shop = new Shop(new FastPayClient());error CS1503: Argument 1: cannot convert from 'FastPayClient' to 'IPaymentGateway'La biblioteca es código ajeno: no puedes añadirle un método. Reescribir la tienda para FastPay significa tocar cada lugar que paga, y volver a tocarlo con el siguiente proveedor.
La idea
Dos envoltorios, dos objetivos distintos
Los dos patrones de esta lección envuelven código ajeno, pero resuelven problemas distintos.
Un Adapter es una clase con la forma que espera tu código, que por dentro llama al objeto ajeno. Traduce los nombres, las unidades y el resultado. Como un adaptador de enchufe de viaje: el enchufe de tu cargador no entra en la toma de otro país, el adaptador encaja con las dos. No añade nada, solo cambia la forma.
Una Facade es un único punto de entrada sencillo delante de muchas clases que hay que usar en un orden determinado. Como la recepción de un hotel: no llamas por separado a la limpieza, a la cocina y a contabilidad, se lo dices a recepción.
La diferencia en breve:
- un adapter cambia la forma de una interfaz para que encaje con otra;
- una facade simplifica el trabajo con muchas clases poniendo la secuencia habitual detrás de un método.
Míralo
El adapter traduce
FastPayAdapter implementa IPaymentGateway. Por dentro guarda el cliente de FastPay y traduce: euros a céntimos, "EUR" y opciones vacías, el texto a sí o no:
class FastPayAdapter : IPaymentGateway{ private readonly FastPayClient client;
public FastPayAdapter(FastPayClient client) { this.client = client; }
public bool Charge(decimal amount) { long cents = (long)Math.Round(amount * 100, MidpointRounding.AwayFromZero); string status = client.MakePayment("EUR", cents, new Dictionary<string, string>()); return status == "APPROVED"; }}La tienda recibe el adapter en lugar del cliente. En ella no cambia nada:
var shop = new Shop(new FastPayAdapter(new FastPayClient()));shop.Pay("#1042", 19.99m);shop.Pay("#1043", 1299m);FastPay: 1999 EUR#1042: pagadoFastPay: 129900 EUR#1043: rechazadoEl televisor de 1299 € supera el límite de FastPay (1000 €), así que se rechaza. La tienda se entera en su propio idioma: false.
La escena muestra el adapter y después la facade de la tarjeta siguiente:
var payments = new FastPayAdapter(new FastPayClient());Console.WriteLine(payments.Charge(19.99m));
var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());checkout.PlaceOrder("Hervidor", 24.90m);Paso a paso, en palabras
- La tienda llama a Charge(importe en euros). FastPayClient solo tiene MakePayment(moneda, céntimos, opciones), y la llamada directa rebota.
- FastPayAdapter se pone en medio: por fuera es IPaymentGateway con Charge, y por dentro guarda el cliente de FastPay.
- Charge(19.99): el adaptador redondea los euros a céntimos, 1999, y llama a MakePayment con "EUR".
- FastPay responde "APPROVED", y el adaptador lo convierte en True: el idioma de la tienda.
- CheckoutFacade recibe el almacén, los pagos (el mismo adaptador), las facturas y el correo.
- PlaceOrder: primero el almacén reserva el hervidor.
- Luego el pago, otra vez por el adaptador: 24.90 € se convierten en 2490 céntimos.
- Pasó, así que la Facade emite la factura y envía el correo. Quien llama escribió una sola línea.
Más precisamente, para curiosos
- .NET también tiene adapters.
StreamReaderconvierte un flujo de bytes (Stream) en un lector de texto, yReadOnlyCollection<T>muestra una lista como una colección de solo lectura. - El adapter guarda el objeto ajeno en un campo (composición). También podría heredar de él, pero entonces la tienda ve además todos los métodos de FastPay, y un adapter no puede envolver un cliente que ya existe.
Míralo
La facade delante de cuatro clases
El pago es solo un paso de un pedido. Antes, el almacén reserva el artículo, y después vienen la factura y el correo. Si el pago se rechaza, el almacén libera el artículo. Cada paso es una clase pequeña:
class Stock{ public bool Reserve(string item) { Console.WriteLine("Almacén: reservo " + item); return true; }
public void Release(string item) { Console.WriteLine("Almacén: libero " + item); }}
class Invoices{ public void Issue(string item, decimal amount) { Console.WriteLine($"Factura: {item}, {amount:F2} €"); }}
class Mailer{ public void Send(string text) { Console.WriteLine("Correo: " + text); }}Sin facade, cada lugar que recibe un pedido (el sitio web, la aplicación móvil, los pedidos por teléfono) escribe lo mismo, en el mismo orden:
if (stock.Reserve(item)){ if (payments.Charge(amount)) { invoices.Issue(item, amount); mailer.Send("confirmación de " + item); } else { stock.Release(item); }}Si un lugar olvida stock.Release, el artículo queda reservado para un pedido que nunca se pagó. CheckoutFacade escribe el orden una sola vez:
class CheckoutFacade{ private readonly Stock stock; private readonly IPaymentGateway payments; private readonly Invoices invoices; private readonly Mailer mailer;
public CheckoutFacade(Stock stock, IPaymentGateway payments, Invoices invoices, Mailer mailer) { this.stock = stock; this.payments = payments; this.invoices = invoices; this.mailer = mailer; }
public bool PlaceOrder(string item, decimal amount) { if (!stock.Reserve(item)) { return false; } if (!payments.Charge(amount)) { stock.Release(item); return false; } invoices.Issue(item, amount); mailer.Send("confirmación de " + item); return true; }}Los pagos de la facade son un IPaymentGateway, que se le pasa desde fuera (inyección de dependencias, de la lección «Singleton e inyección de dependencias»). Aquí es el adapter de la tarjeta anterior. Los dos patrones trabajan juntos:
var payments = new FastPayAdapter(new FastPayClient());var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());checkout.PlaceOrder("Hervidor", 24.90m);Almacén: reservo HervidorFastPay: 2490 EURFactura: Hervidor, 24.90 €Correo: confirmación de HervidorUna facade no es un muro. Las clases de detrás se pueden seguir usando directamente cuando necesitas algo fuera del camino habitual. Contabilidad vuelve a emitir la factura:
var invoices = new Invoices();invoices.Issue("Hervidor", 24.90m);Factura: Hervidor, 24.90 €Pruébalo
Predice la salida
Las clases son de las tarjetas «El adapter traduce» y «La facade delante de cuatro clases». ¿Qué imprime el programa?
var payments = new FastPayAdapter(new FastPayClient());var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());checkout.PlaceOrder("Televisor", 1299m);Pruébalo
Encuentra el error
var shop = new Shop(new FastPayAdapter(new FastPayClient()));shop.Pay("#1044", 0.375m * 13.99m);FastPay: 524 EUR#1044: pagadoElige la línea con el error.
(long) no redondea, corta la parte decimal. decimal guarda 5.24625 con exactitud, pero 524.625 céntimos se cortan a 524. La corrección es (long)Math.Round(amount * 100, MidpointRounding.AwayFromZero): la mitad sube, 525. Sin el segundo argumento, Math.Round redondea las mitades al par (524.5 pasa a 524), como hacen los bancos, y la tienda debería elegir su regla de forma explícita.Más precisamente, para curiosos
Pruébalo tú mismo:
Console.WriteLine(0.375m * 13.99m * 100);524.62500decimal y no double: con double, 19.99 * 100 da 1998.9999999999998, igual que en Java. decimal conserva los dígitos decimales con exactitud, pero las fracciones de céntimo siguen teniendo que redondearse, y la regla (MidpointRounding) se escribe de forma explícita.La idea
Cuándo no los necesitas
No necesitas un adapter cuando los dos lados son código tuyo. Entonces cambia uno de los lados para que encaje con el otro, en lugar de poner un traductor en medio. Un adapter es para código que no puedes tocar: una biblioteca ajena, un sistema antiguo, el servicio de otro equipo.
No necesitas una facade cuando la secuencia se usa en un solo lugar. Ahí basta una función. Una facade también tiene una trampa: se convierte fácilmente en la clase por la que pasa todo, con un método para cada necesidad de cada pantalla. Deja en ella solo los caminos habituales y llama directamente a las clases para el resto.
Los dos patrones son delgados. Un adapter que empieza a decidir reglas de negocio (descuentos, límites) ya no es un adapter. Una facade que calcula importes ella misma en lugar de llamar a las clases que lo hacen, tampoco.
Ejercicio
Un segundo proveedor
Resuélvelo en tu propio editor.
PayNow. La empresa añade un segundo proveedor, PayNow. Su biblioteca quiere los céntimos primero y la moneda después, y devuelve un número: 0 significa éxito, cualquier otro es un rechazo.
class PayNowApi{ public int Send(int cents, string currency) { Console.WriteLine($"PayNow: {currency} {cents}"); return 0; }}Escribe PayNowAdapter para que la tienda pague con PayNow sin cambios:
var shop = new Shop(new PayNowAdapter(new PayNowApi()));shop.Pay("#1042", 19.99m);PayNow: EUR 1999#1042: pagadoMás tareas
- Cancelar un pedido. Añade CancelOrder(item) a la facade: el almacén libera el artículo y el cliente recibe un correo «pedido cancelado de …».
- Un proveedor de prueba. Escribe
FakeGateway : IPaymentGatewayque rechace cualquier importe superior a 50 €. Pásaselo a la facade y comprueba que el almacén libera el artículo cuando hay un rechazo. - La factura falla. Haz que emitir la factura lance una excepción. El dinero ya se cobró. ¿Qué debería hacer la facade? Decídelo y escríbelo: por ejemplo, seguir con el correo y registrar el error para emitir la factura más tarde.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Patrones de diseño · Listas, diccionarios y LINQ · Strategy · Singleton e inyección de dependencias
- Continúa en: La tienda: qué patrón dónde · Arquitectura hexagonal y clean architecture · Integrar sistemas antiguos y ajenos
- Ejercicios sobre el tema: Patrones de diseño