Adapter and Facade
In short: Adapter translates a foreign interface into the one your code expects, while Facade hides several subsystems (stock, payment, invoice, email) behind a single method.
Problem
The new provider doesn't fit
The shop pays through its own interface: charge takes the amount in euros and says whether the payment went through. The shop doesn't know who is behind it:
class Shop: def __init__(self, gateway): self.gateway = gateway
def pay(self, order, amount): if self.gateway.charge(amount): print(order + ": paid") else: print(order + ": declined")charge method.The company switches its payment provider to FastPay, which charges lower fees. FastPay ships a ready-made library:
# The FastPay library: foreign code you can't change.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"It does the same job, in a different shape. The method is called make_payment, wants a currency, an amount in cents and options, and returns the text "APPROVED" or "DECLINED" instead of yes or no. Handed straight to the shop, it doesn't fit:
shop = Shop(FastPayClient())shop.pay("#1042", 19.99)AttributeError: 'FastPayClient' object has no attribute 'charge'The library is foreign code: you can't add a method to it. Rewriting the shop for FastPay means touching every place that pays, and touching it again with the next provider.
The idea
Two wrappers, two different goals
Both patterns in this lesson wrap foreign code, but they solve different problems.
An Adapter is a class with the shape your code expects, which calls the foreign object inside. It translates the names, the units and the result. Like a travel plug adapter: your charger's plug doesn't fit the socket abroad, the adapter fits both. It adds nothing, it only changes the shape.
A Facade is one simple entry point in front of many classes that have to be used in a certain order. Like a hotel reception: you don't call the cleaners, the kitchen and accounting one by one, you tell the reception.
The difference in short:
- an adapter changes the shape of one interface so it fits another;
- a facade simplifies working with many classes by putting the usual sequence behind one method.
See it
The adapter translates
FastPayAdapter has a charge method, exactly what the shop expects. Inside, it keeps the FastPay client and translates: euros to cents, "EUR" and empty options, the text to yes or 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"The shop gets the adapter instead of the client. Nothing in it changes:
shop = Shop(FastPayAdapter(FastPayClient()))shop.pay("#1042", 19.99)shop.pay("#1043", 1299)FastPay: 1999 EUR#1042: paidFastPay: 129900 EUR#1043: declinedThe €1299 TV is above FastPay's limit (€1000), so it is declined. The shop learns about it in its own language: False.
The scene shows the adapter, then the facade from the next card:
payments = FastPayAdapter(FastPayClient())print(payments.charge(19.99))
checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Kettle", 24.90)Step by step, in words
- The shop calls charge(amount in euros). FastPayClient only has make_payment(currency, cents, options), so a direct call bounces off.
- FastPayAdapter steps in between: outside it has charge, just as the shop expects, inside it keeps the FastPay client.
- charge(19.99): the adapter rounds euros to cents, 1999, and calls make_payment with "EUR".
- FastPay answers "APPROVED", and the adapter turns it into True: the shop's own language.
- CheckoutFacade gets the warehouse, the payments (the same adapter), the invoices and the email.
- place_order: first the warehouse reserves the kettle.
- Then the payment, through the adapter again: €24.90 becomes 2490 cents.
- It went through, so the facade issues the invoice and sends the email. The caller wrote one line.
More precisely, if you are curious
- Python doesn't check types: the shop calls
chargeon whatever you give it. The adapter only needs achargemethod with the right parameters and result. With a foreign object, the error comes only at payment time (AttributeError), not at startup, so a test matters. - If you want the shape written down,
typing.Protocoldescribes "an object with acharge(amount) -> boolmethod", and a type checker (mypy) checks it before the program runs.
See it
The facade in front of four classes
The payment is only one step of an order. Before it, the warehouse reserves the item, and after it come the invoice and the email. If the payment is declined, the warehouse releases the item. Each step is a small class:
class Stock: def reserve(self, item): print("Warehouse: reserving " + item) return True
def release(self, item): print("Warehouse: releasing " + item)
class Invoices: def issue(self, item, amount): print(f"Invoice: {item}, €{amount:.2f}")
class Mailer: def send(self, text): print("Email: " + text)Without a facade, every place that takes an order (the website, the mobile app, phone orders) writes the same thing, in the same order:
if stock.reserve(item): if payments.charge(amount): invoices.issue(item, amount) mailer.send("confirmation for " + item) else: stock.release(item)If one place forgets stock.release, the item stays reserved for an order that was never paid. CheckoutFacade writes the order once:
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("confirmation for " + item) return TrueThe payments in the facade are an object with charge, handed in from outside (dependency injection from the lesson "Singleton and Dependency Injection"). Here it is the adapter from the previous card. The two patterns work together:
payments = FastPayAdapter(FastPayClient())checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Kettle", 24.90)Warehouse: reserving KettleFastPay: 2490 EURInvoice: Kettle, €24.90Email: confirmation for KettleA facade isn't a wall. The classes behind it can still be used directly when you need something off the usual path. Accounting issues the invoice again:
invoices = Invoices()invoices.issue("Kettle", 24.90)Invoice: Kettle, €24.90Try it
Predict the output
The classes are from the cards "The adapter translates" and "The facade in front of four classes". What does the program print?
payments = FastPayAdapter(FastPayClient())checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("TV", 1299)Try it
Find the bug
shop = Shop(FastPayAdapter(FastPayClient()))shop.pay("#1042", 19.99)FastPay: 1998 EUR#1042: paidPick the line with the bug.
int doesn't round, it cuts off the fraction. And 19.99 in a float isn't exactly 19.99: floating-point numbers are stored in binary, and 19.99 * 100 gives 1998.9999999999998. Cut off, that is 1998. The fix is round(amount * 100): the nearest whole number, 1999. A cent looks small, but it is wrong on every such amount, and the shop's books and FastPay's don't match.More precisely, if you are curious
Try it yourself:
print(19.99 * 100)1998.9999999999998Python's round rounds halves to even: round(2.5) is 2. For money, where the half goes up, there is Decimal, which keeps the amount exact:
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))2 413The idea
When you don't need them
You don't need an adapter when both sides are your own code. Then change one side to fit the other instead of putting a translator between them. An adapter is for code you can't touch: a foreign library, an old system, another team's service.
You don't need a facade when the sequence is used in one place. A single function there is enough. A facade also has a trap: it easily becomes the class everything goes through, with a method for every need of every screen. Keep only the usual paths in it, and call the classes directly for the rest.
Both patterns are thin. An adapter that starts deciding business rules (discounts, limits) isn't an adapter anymore. Neither is a facade that computes amounts itself instead of calling the classes that do.
Exercise
A second provider
Solve it in your own editor.
PayNow. The company adds a second provider, PayNow. Its library wants the cents first and the currency second, and returns a number: 0 means success, anything else is a decline.
class PayNowApi: def send(self, cents, currency): print(f"PayNow: {currency} {cents}") return 0Write PayNowAdapter so the shop pays through PayNow without changes:
shop = Shop(PayNowAdapter(PayNowApi()))shop.pay("#1042", 19.99)PayNow: EUR 1999#1042: paidMore tasks
- Cancelling an order. Add cancel_order(item) to the facade: the warehouse releases the item, and the customer gets an email "order cancelled for …".
- A test provider. Write a
FakeGatewaywith achargemethod that declines any amount over €50. Hand it to the facade and check that the warehouse releases the item on a decline. - The invoice fails. Let issuing the invoice throw an exception. The money has already been taken. What should the facade do? Decide and write it: for example, carry on with the email and record the error so the invoice is issued later.
Test
Test yourself
Related lessons
Section titled "Related lessons"- Builds on: dict and set · Design patterns · Strategy · Singleton and Dependency Injection
- Continues in: The shop: which pattern where · Hexagonal and clean architecture · Integrating old and foreign systems
- Tasks on this topic: Design patterns