Card 04/ 08
Interpreter Example: Evaluating Discount Rules
"total over 100 and member" becomes a tree of And, TotalOver and IsMember.
A shop's discount rules form a natural Interpreter, and this example maps the template onto them. A rule is a condition on a basket, or two rules joined by and or or. Each becomes a class, and two rules are evaluated against three baskets.
| Class | Kind | Evaluates to true when |
|---|---|---|
TotalOver | Terminal | The basket total is above the amount |
IsMember | Terminal | The customer is a member |
And | Non-terminal | Both of its parts are true |
Or | Non-terminal | Either of its parts is true |
Basket | Context | Not an expression; it is what the rules are evaluated against |
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass(frozen=True)
class Basket:
total: float
member: bool
class Rule(ABC):
@abstractmethod
def interpret(self, basket: Basket) -> bool: ...
class TotalOver(Rule):
def __init__(self, amount: float) -> None:
self.amount = amount
def interpret(self, basket: Basket) -> bool:
return basket.total > self.amount
class IsMember(Rule):
def interpret(self, basket: Basket) -> bool:
return basket.member
class And(Rule):
def __init__(self, left: Rule, right: Rule) -> None:
self.left = left
self.right = right
def interpret(self, basket: Basket) -> bool:
return self.left.interpret(basket) and self.right.interpret(basket)
class Or(Rule):
def __init__(self, left: Rule, right: Rule) -> None:
self.left = left
self.right = right
def interpret(self, basket: Basket) -> bool:
return self.left.interpret(basket) or self.right.interpret(basket)
member_discount = And(TotalOver(100), IsMember())
free_delivery = Or(TotalOver(50), IsMember())
for basket in [Basket(120, True), Basket(120, False), Basket(30, True)]:
print(
f"total {basket.total:>5}, member {basket.member!s:<5}:",
f"member discount {member_discount.interpret(basket)!s:<5}",
f"free delivery {free_delivery.interpret(basket)}",
)total 120, member True : member discount True free delivery True
total 120, member False: member discount False free delivery True
total 30, member True : member discount False free delivery TrueA member spending 120 gets both the discount and free delivery. A non-member spending 120 gets free delivery only, and a member spending 30 gets free delivery because Or needs only one part to be true.
The terminal classes are the only ones that read the basket. And and Or know nothing about totals or membership; they combine whatever their parts return, so a new condition, such as a discount code, is one new terminal class that both can hold at once.
← → move through sections and cards