🐍 Python - Strategy Pattern
Updated at 2018-11-21 02:20
Strategy is selecting an approach (e.g. a function) to use at runtime.
We were marking strategies with decorators but there are multiple ways to define them.
- the naive way is just to list all the promotions but this is error-prone;
promotions = [bonus_promotion, bulk_item_promotion, large_cart_promotion]
- loading strategies by name in this namespace;
promotions = [globals()[name] for name in globals() if name.endswith('_promotion') and name != 'best_promotion']
- loading strategies from a module;
promotions = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
- loading strategies from subclasses;
Promotion.__subclasses__()
, note that you need to execute the module containing the subclasses for this to work
from dataclasses import dataclass
from typing import Callable, Collection
@dataclass
class Customer():
name: str
bonus_points: int
@dataclass
class LineItem():
product: str
quantity: int
price: float
def total(self):
return self.price * self.quantity
class Order:
def __init__(
self,
customer: Customer,
cart: Collection[LineItem] = None,
promotion: Callable[['Order'], float] = None,
):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
self.total = sum(item.total() for item in self.cart)
self.due = self._due()
def _due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total - discount
def __repr__(self):
name = self.__class__.__name__
return f'<{name} total:{self.total!r} due:{self.due!r}>'
promotions = []
def promotion(promotion_func):
promotions.append(promotion_func)
return promotion_func
@promotion
def bonus_promotion(order: Order) -> float:
""" Customer gets 5% discount if they have 1000 bonus points. """
return order.total * .05 if order.customer.bonus_points >= 1000 else 0
@promotion
def bulk_item_promotion(order: Order) -> float:
""" 10% discount when buying 20 or more of a specific item. """
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@promotion
def large_cart_promotion(order: Order) -> float:
""" 7% discount for orders with 10 or more distinct items. """
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total * .07
return 0
bob = Customer('Bob Doe', 0)
alice = Customer('Alice Cooper', 1100)
# Usage #1
basic_cart = [
LineItem('orange', 4, .5),
LineItem('apple', 10, 1.5),
LineItem('watermelon', 5, 5.0),
]
assert str(Order(bob, basic_cart, bonus_promotion)) == '<Order total:42.0 due:42.0>'
assert str(Order(alice, basic_cart, bonus_promotion)) == '<Order total:42.0 due:39.9>'
# Usage #2
banana_cart = [
LineItem('banana', 30, .5),
LineItem('apple', 5, 1.5),
]
assert str(Order(bob, basic_cart, bulk_item_promotion)) == '<Order total:42.0 due:42.0>'
assert str(Order(alice, banana_cart, bulk_item_promotion)) == '<Order total:22.5 due:21.0>'
# Usage #3
long_order = [LineItem(str(item_name), 1, 1.0) for item_name in range(10)]
assert str(Order(bob, long_order, large_cart_promotion)) == '<Order total:10.0 due:9.3>'
# Usage #4
def best_promotion(order: Order) -> float:
return max(p(order) for p in promotions)
assert str(Order(alice, basic_cart, best_promotion)) == '<Order total:42.0 due:39.9>'
Source
- Python Tricks The Book, Dan Bader
- Fluent Python, Luciano Ramalho