Card 06/ 08
An Adapter That Grows Business Logic
The adapter starts validating orders and applying discounts, and now two classes do the gateway's job.
An adapter translates one interface into another and does nothing else, and the common way to break it is to let rules about the business into the translation. The adapter is the one class that sees every payment, so it looks like a convenient place for a check or a discount. This card shows that happening, and why the rule belongs somewhere else.
Logic in the adaptera rule nobody looks for here
def pay(self, amount):
if amount > Decimal("500"):
raise ValueError("needs approval")
if date.today().month == 12:
amount = amount * Decimal("0.9")
cents = int(amount * 100)
response = self._gateway.make_transaction(cents, self._currency)
return response["status"] == "OK"Checkouts that use another gateway skip the approval check and the December discount.
Translation onlythe rules live with the checkout
def pay(self, amount):
cents = int(amount * 100)
response = self._gateway.make_transaction(cents, self._currency)
return response["status"] == "OK"Swap the gateway and every rule still applies, because none was in the adapter.
Three things go wrong when the adapter holds rules. The rules apply only to payments through this gateway, so adding a second provider drops them without any error. They are hidden, because nobody reading the checkout's discount code thinks to open an adapter. And the adapter's tests now need dates and limits, when they should only check that pounds became pence.
The test for any line in an adapter is whether it would still be needed if the adaptee already had the target's interface. A unit conversion would not, so it belongs. An approval limit would, so it belongs in the client or in a service the client calls, where every gateway passes through it.
← → move through sections and cards