Card 08/ 09
Practice: A Savings Account That Refuses Withdrawals
A subclass that raises on withdraw(), and a payment service that calls it.
Recognising and fixing a broken substitution is the skill practised here. A bank's Account has a withdraw method, and a FixedTermSavings subclass locks the money until the term ends. A PaymentService takes money from any account it is given. Answer three questions: what happens when the service is given a fixed-term account, which Liskov rule is broken, and how would you fix the hierarchy?
class Account:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
class FixedTermSavings(Account):
def withdraw(self, amount):
raise PermissionError("withdrawals are locked until the term ends")
class PaymentService:
def pay(self, account, amount):
account.withdraw(amount)
return f"paid {amount}, balance {account.balance}"
service = PaymentService()
print(service.pay(Account(500), 120))
try:
print(service.pay(FixedTermSavings(500), 120))
except PermissionError as error:
print(f"PermissionError: {error}")paid 120, balance 380
PermissionError: withdrawals are locked until the term endsShow the answers
What happens. The ordinary account pays and its balance drops to 380. The fixed-term account raises PermissionError from inside pay, so the service fails on an account its type said was acceptable.
The rule broken. Account.withdraw promises to reduce the balance or raise ValueError for insufficient funds. The subclass throws a new kind of exception for a reason the parent never had, which weakens the postcondition. A caller that handled ValueError correctly is still broken.
The fix. Change the abstraction. Not every account can be withdrawn from, so withdraw does not belong in Account. Move it into a WithdrawableAccount, make the current account extend that, make FixedTermSavings extend Account only, and make PaymentService.pay accept a WithdrawableAccount. A fixed-term account can then no longer be passed to it, and Java, TypeScript or a Python type checker will say so before the code runs.
← → move through sections and cards