Design Patterns
STRATEGY Pattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 | from abc import ABC, abstractmethod
from collections import namedtuple
Customer = namedtuple("Customer", "name fidelity")
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order:
"""This is the CONTEXT part of the Strategy-Pattern"""
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, "__total"):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
fmt = "< Order total = {:.2f}; DUE = {:.2f} >"
return fmt.format(self.total(), self.due())
class Promotion(ABC):
"""
The STRATEGY part of the Strategy-pattern
An Abstract Base Class
"""
@abstractmethod
def discount(self, order):
"""Return discount as a positive dollar amount"""
class FidelityPromot(Promotion):
"""
First CONCRETE implementation of STRATEGY ABC
5% disount for customer with 1000 or more fidelity points
"""
def discount(self, order):
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
class BulkPromo(Promotion):
"""
Second CONCRETE implementation of the Strategy-pattern
10% discount for each line-item with 20 or more units
"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
class LargeOrderPromo(Promotion):
"""
Third CONCRETE implementation of the Strategy-pattern
7% discount for orders with 10 or more distinct items
"""
def discount(self, order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
|
Example Usage
Sample usage of Order
class with different promotions applied
| joe = Customer("John Doe", 0)
ann = Customer("Ann Smith", 1100)
cart = [LineItem("banana", 4, 0.5),
LineItem("apple", 10, 1.5),
LineItem("watermelon", 5, 5.0)]
|
| Order(joe, cart, FidelityPromo()) ## < Order total = 42.00; DUE = 42.00 >
Order(ann, cart, FidelityPromo()) ## < Order total = 42.00; DUE = 39.90 >
|
Few more example usage with differnt cart types
| banana_cart = [LineItem("banana", 30, 0.5),
LineItem("apple", 10, 1.5)]
Order(joe, banana_cart, BulkItemPromo()) ## < Order total = 30.00; DUE = 28.50 >
|
| long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
Order(joe, long_order, LargeOrderPromo()) ## < Order total = 10.00; DUE = 9.30 >
Order(joe, cart, LargeOrderPromo()) ## < Order total = 42.00; DUE = 42.00 >
|
Function-oriented STRATEGY Pattern
Each concrete implementation of the Strategy Pattern in above code is a class
with a single method discount()
. Furthermore, the strategy instances have no state
(i.e. no instance attributes).
They look a lot like plain functions.
So, below we re-write the concrete implementations of the Strategy Pattern as plain function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 | from collections import namedtuple
Customer = namedtuple("Customer", "name fidelity")
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order:
"""The CONTEXT"""
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, "__total"):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
discount = 0
if self.promotion:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
fmt = "<Order total = {:.2f}; DUE = {:.2f}>"
fmt.format(self.total(), self.due())
########################################################################################
## Redesign of the concrete-implementations of STRATEGY PATTERN as functions
def fidelity_promot(order):
"""5% discount for customers with >= 1000 fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
"""10% discount for each LineItem with >= 20 units in cart"""
discount = 0
for item in oder.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
def large_order_promo(order):
"""7% discount for orders with >= 10 distinct items"""
distinct_items = set(item.product for item in order.cart)
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
|
Example Usage
Smaple usage examples of Order
class with promotion Strategy as functions
| joe = Customer("John Doe", 0)
ann = Customer("Ann Smith", 1100)
cart = [LineItem("banana", 4, 0.5),
LineItem("apple", 10, 1.5),
LineItem("watermelon", 5, 5.0)]
|
| Order(joe, cart, fidelity_promo) ## < Order total = 42.00; DUE = 42.00 >
Order(ann, cart, fidelity_promo) ## < Order total = 42.00; DUE = 39.90 >
|
Another Example
| banana_cart = [LineItem("banana", 30, 0.5),
LineItem("apple", 10, 1.5)]
Order(joe, banana_cart, bulk_item_promo) ## < Order total = 30.00; DUE = 28.50 >
|
Yet another Example
| long_order = [LineItem(str(item_id), 1, 1.0) for item_id in range(10)]
Order(joe, long_order, large_order_promo) ## < Order total = 10.00; DUE = 9.30 >
Order(joe, cart, large_order_promo) ## < Order total = 42.00; DUE = 42.00 >
|
- STRATEGY objects often make good FLYWEIGHTS
- A FLYWIGHT is a shared object that cane be use din multiple contexts simulatenously.
- Sharing is encouraged to reduce the creation of a new concrete strategy object when the
same strategy is applied over and over again in different contexts (i.e. with every new
Order
instance)
- If the strategies have no internal state (often the case);
then use plain old functions else adapt to use class version.
- A function is more lightweight than an user-defined
class
- A plain functions is also a_shared_ object that can be used in multiple contexts simulateneously.
Choosing the best Strategy
Given the same customers and carts from above examples; we now add additional tests.
| Order(joe, long_order, best_promo) ## < Order total = 10.00; DUE = 9.30 > ## case-1
Order(joe, banana_cart, best_promo) ## < Order total = 30.00; DUE = 28.50 > ## case-2
Order(ann, cart, best_promo) ## < Order total = 42.00; DUE = 39.90 > ## case-3
|
- case-1:
best_promo
selected the large_order_promo
for customer joe
- case-2:
best_promo
selected the bulk_item_promo
for customer joe
(for ordering lots of bananas)
- case-3:
best_promo
selected the fidelity_promo
for ann
's loyalty.
Below is the implementation of best_promo
| all_promos = [fidelity_promo, bulk_item_promo, large_order_promo]
def best_promo(order):
"""Selects the best discount avaailable. Only one discount applicable"""
best_discount = max(promo(order) for promo in all_promos)
return best_discount
|
Finding Strategies in a module
| ## Method-1
## using globals()
all_promos = [globals()[name] for name in globals()
if name.endswith("_promo")
and name != "best_promo"]
def best_promo(order):
best_discount = max(promo(order) for order in all_promos)
return best_discount
|
But a more flexible way to handle this is using inbuilt inspect
module
and storing all the promos functions in a file promotions.py
.
This works regardless of the names given to promos.
| ## Method-2
## using modules to store the promos separately
all_promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
best_discount = max(promo(order) for promo in all_promos)
return best_discount
|
Both the methods have pros & cons. Choose as you see fit.
We could add more stringent tests to filter the functions,
by inspecting their arguments for instance.
A more elegant solution would be use a decorator. (we will study this in later blogs)
References