Ir al contenido
wannacode

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:

class Shop:
def __init__(self, gateway):
self.gateway = gateway
def pay(self, order, amount):
if self.gateway.charge(amount):
print(order + ": pagado")
else:
print(order + ": rechazado")
En Python no hay interfaces: la tienda acepta cualquier objeto que tenga un método charge.

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:
def make_payment(self, currency, amount_in_cents, options):
print(f"FastPay: {amount_in_cents} {currency}")
if amount_in_cents > 100000:
return "DECLINED"
return "APPROVED"

Hace el mismo trabajo, con otra forma. El método se llama make_payment, 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:

shop = Shop(FastPayClient())
shop.pay("#1042", 19.99)
Error
AttributeError: 'FastPayClient' object has no attribute 'charge'

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 tiene un método charge, justo lo que espera la tienda. 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:
def __init__(self, client):
self.client = client
def charge(self, amount):
cents = round(amount * 100)
status = self.client.make_payment("EUR", cents, {})
return status == "APPROVED"

La tienda recibe el adapter en lugar del cliente. En ella no cambia nada:

shop = Shop(FastPayAdapter(FastPayClient()))
shop.pay("#1042", 19.99)
shop.pay("#1043", 1299)
Salida
FastPay: 1999 EUR
#1042: pagado
FastPay: 129900 EUR
#1043: rechazado

El 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:

payments = FastPayAdapter(FastPayClient())
print(payments.charge(19.99))
checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())
checkout.place_order("Hervidor", 24.90)
Paso a paso, en palabras
  1. La tienda llama a charge(importe en euros). FastPayClient solo tiene make_payment(moneda, céntimos, opciones), y la llamada directa rebota.
  2. FastPayAdapter se pone en medio: por fuera tiene charge, como espera la tienda, y por dentro guarda el cliente de FastPay.
  3. charge(19.99): el adaptador redondea los euros a céntimos, 1999, y llama a make_payment con "EUR".
  4. FastPay responde "APPROVED", y el adaptador lo convierte en True: el idioma de la tienda.
  5. CheckoutFacade recibe el almacén, los pagos (el mismo adaptador), las facturas y el correo.
  6. place_order: primero el almacén reserva el hervidor.
  7. Luego el pago, otra vez por el adaptador: 24.90 € se convierten en 2490 céntimos.
  8. 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
  • Python no comprueba tipos: la tienda llama a charge en lo que le des. El adapter solo necesita un método charge con los parámetros y el resultado correctos. Con un objeto ajeno, el error llega solo al pagar (AttributeError), no al arrancar, así que un test importa.
  • Si quieres dejar la forma por escrito, typing.Protocol describe «un objeto con un método charge(amount) -> bool», y un verificador de tipos (mypy) lo comprueba antes de ejecutar el programa.

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:
def reserve(self, item):
print("Almacén: reservo " + item)
return True
def release(self, item):
print("Almacén: libero " + item)
class Invoices:
def issue(self, item, amount):
print(f"Factura: {item}, {amount:.2f} €")
class Mailer:
def send(self, text):
print("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:
def __init__(self, stock, payments, invoices, mailer):
self.stock = stock
self.payments = payments
self.invoices = invoices
self.mailer = mailer
def place_order(self, item, amount):
if not self.stock.reserve(item):
return False
if not self.payments.charge(amount):
self.stock.release(item)
return False
self.invoices.issue(item, amount)
self.mailer.send("confirmación de " + item)
return True

Los pagos de la facade son un objeto con charge, 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:

payments = FastPayAdapter(FastPayClient())
checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())
checkout.place_order("Hervidor", 24.90)
Salida
Almacén: reservo Hervidor
FastPay: 2490 EUR
Factura: Hervidor, 24.90 €
Correo: confirmación de Hervidor

Una 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:

invoices = Invoices()
invoices.issue("Hervidor", 24.90)
Salida
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?

payments = FastPayAdapter(FastPayClient())
checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())
checkout.place_order("Televisor", 1299)
Respuesta

Pruébalo

Encuentra el error

Un compañero «simplificó» el adapter: los céntimos son un número entero de todos modos. Un cliente paga el pedido #1042 por 19,99 €:
shop = Shop(FastPayAdapter(FastPayClient()))
shop.pay("#1042", 19.99)
Salida
FastPay: 1998 EUR
#1042: pagado
FastPay cobró 1998 céntimos, uno menos. ¿En qué línea del adapter está el error?

Elige la línea con el error.

Más precisamente, para curiosos

Pruébalo tú mismo:

print(19.99 * 100)
Salida
1998.9999999999998

El round de Python redondea las mitades al par: round(2.5) es 2. Para dinero, donde la mitad sube, existe Decimal, que guarda el importe con exactitud:

print(round(2.5), round(3.5))
from decimal import Decimal, ROUND_HALF_UP
print((Decimal("0.125") * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
Salida
2 4
13

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:
def send(self, cents, currency):
print(f"PayNow: {currency} {cents}")
return 0

Escribe PayNowAdapter para que la tienda pague con PayNow sin cambios:

shop = Shop(PayNowAdapter(PayNowApi()))
shop.pay("#1042", 19.99)
Salida
PayNow: EUR 1999
#1042: pagado

Más tareas

  • Cancelar un pedido. Añade cancel_order(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 un FakeGateway con un método charge que 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

1. La biblioteca del proveedor tiene un método con otro nombre, otras unidades y otro resultado que los que espera tu código. ¿Qué patrón encaja?
2. El sitio web, la aplicación móvil y los pedidos por teléfono repiten los mismos cuatro pasos en el mismo orden. ¿Qué patrón encaja?
3. Con una facade, ¿se pueden seguir usando directamente el almacén y las facturas?
4. El adapter convierte 19,99 € en céntimos cortando la parte decimal. ¿Qué recibe el proveedor?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·