Chapter 6
章前占位。- -!
6.1 使用一等函数实现设计模式
作为一等对象的函数称为 一等函数。
6.1.1 案例分析:重构 策略 模式
图 6-1 中的 UML 类图指出了 策略 模式对类的编排。

上图中的上下文指的是调用 Order 类的其他代码。
电商中根据客户的属性或订单中的商品计算折扣功能明显可以使用 策略 模式。
案例:
假如一个网店制定了下述折扣规则:
- 有 1000 或以上积分的顾客,每个订单享 5% 折扣
- 同一订单中,单位商品的数量达到 20 个或以上,享 10% 折扣
- 订单中的不同商品达到 10 个或以上,享 7% 折扣
分析:
上下文。案例中与上下文有联系的是订单类,即 Order
策略。使用名为 Promotion 的抽象类为实现不同策略提供共同接口
具体策略。策略类(即 Promotion 类)的具体子类的。fidelityPromo、BulkPromo 和 LargeOrderPromo 三个类实现具体策略。
代码:
from abc import ABC, abstractmethod
from collections import namedtuple
Customer = namedtuple("Customer", "name fidelity")
class LineItem:
"""
商品信息类。
属性:商品名 product,数量 quantity,单价 price
方法:计算总价格 total()
"""
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.quantity * self.price
class Order: # 订单
"""
订单类。
属性:客户/消费者 customer, 购物车 cart, 折扣策略 promotion
方法:没有折扣的总价格 total(), 根据不同策略打折后的总价格 due()
重写 __repr__(), 用于显示调 Order 类时的显示内容
"""
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): # 策略:抽象基类
@abstractmethod
def discount(self, order):
"""返回折扣金额(正值)"""
class FidelityPromo(Promotion): # 具体策略一
"""为积分为 1000 或以上的顾客提供 5% 折扣"""
def discount(self, order):
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
class BulkItemPromo(Promotion): # 具体策略二
"""单个商品为 20 个或以上时提供 10% 折扣"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
class LargeOrderPromo(Promotion): # 具体策略三
"""订单中的不同商品达到 10 个或以上时提供 7% 折扣"""
def discount(self, order):
distinct_item = {item.product for item in order.cart} # 可以去重?
if len(distinct_item) >= 10:
return order.total() * .07
return 0
上述代码可以正常运行。
测试代码:
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 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>
>>> banana_cart = [LineItem('banana', 30, .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>
6.1.2 使用函数实现“策略”模式
前例是使用类实现每个具体策略,但是会发现:
- 每个具体策略都只定义了一个方法,即
discount - 每个具体策略的实例没有状态(没有实例属性)
可以把每个具体策略改为简单函数。
代码:
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.quantity * self.price
class Order:
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(self)
def __repr__(self):
msg = "<Order total: {:.2f}, due: {:.2f}>"
return msg.format(self.total(), self.due())
def fidelity_promot(order):
"""为积分为 1000 或以上的顾客提供 5% 折扣"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
"""单个商品为 20 个或以上时提供 10% 折扣"""
discount = 0
for iterm in order.cart:
if iterm.quantity >= 20:
discount += item.total * .1
return discount
def large_order_promo(order):
"""订单中的不同商品达到 10 个或以上时提供 7% 折扣"""
discount_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
测试代码:
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo) # 1
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) # 2
<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, large_order_promo) # 3
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
上面测试代码中 # 1、# 2、# 3 处与使用策略类实现的测试代码中相同位置代码对比少了括号,具体对比:
Order(joe, cart, fidelity_promo) # 函数实现策略
Order(joe, cart, FidelityPromo()) # 类实现策略
6.1.3 选择最佳策略:简单方式
为顾客找到折扣额度最大的策略
promos = [fidelity_promo, bulk_item_promo, larg_order_promo]
def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)
测试代码:
>>> Order(joe, long_order, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo)
<Order total: 42.00 due: 39.90>
6.1.4 找出模块中的全部策略
在 Pytrhon 中,模块也是一等对象,标准库中提供了几个处理模块的函数。
1、globals()
返回一个 字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义它们的模块,而不是调用它们的模块)。
示例:使用 globals 函数帮助 best_promo 自动找到其他可用的 *_promo 函数。
# 内省模块的全局命名空间,构建 promos 列表
promos = [globals() [name] for name in globals() if name.endwith("_promo") and name != "best_promo"]
def best_promo(order):
"""
选择可用的最佳折扣"
"""
return max(promo(order) for promo in promos)
6.2 “命令” 模式
“命令” 设计模式也可以通过把函数作为参数传递而简化。这一模式对类的编排如图 6-2 所示。
