1. 程式人生 > >重構電商例子(策略模式)

重構電商例子(策略模式)

obj item com cached 圖片 子類 tom appid ems

案例分析:重構“策略”模式

技術分享圖片

《設計模式:可復用面向對象軟件的基礎》一書是這樣概述“策略”模式的:

定義一系列算法,把它們一一封裝起來,並且使它們可以相互替換。本模式使得算法可以獨立於使用它的客戶而變化。

假如一個網店制定了下述折扣規則:

  • 有 1000 或以上積分的顧客,每個訂單享 5% 折扣。
  • 同一訂單中,單個商品的數量達到 20 個或以上,享 10% 折扣。
  • 訂單中的不同商品達到 10 個或以上,享 7% 折扣。
  • 簡單起見,我們假定一個訂單一次只能享用一個折扣

“策略”模式的 UML 類圖見圖 6-1,其中涉及下列內容。

內容 說明
上下文 把一些計算委托給實現不同算法的可互換組件,它提供服務。在這個電商示例中,上下文是Order,它會根據不同的算法計算促銷折扣。
策略 實現不同算法的組件共同的接口。在這個示例中,名為 Promotion 的抽象類扮演這個角色。
具體策略 “策略”的具體子類。fidelityPromo、BulkPromo 和 LargeOrderPromo 是這裏實現的三個具體策略。

實現 Order 類,支持插入式折扣策略

# classic_strategy.py
# Strategy pattern -- classic implementation

"""
# BEGIN CLASSIC_STRATEGY_TESTS

    >>> joe = Customer(‘John Doe‘, 0)  # <1>
    >>> ann = Customer(‘Ann Smith‘, 1100)
    >>> cart = [LineItem(‘banana‘, 4, .5),  # <2>
    ...         LineItem(‘apple‘, 10, 1.5),
    ...         LineItem(‘watermellon‘, 5, 5.0)]
    >>> Order(joe, cart, FidelityPromo())  # <3>
    <Order total: 42.00 due: 42.00>
    >>> Order(ann, cart, FidelityPromo())  # <4>
    <Order total: 42.00 due: 39.90>
    >>> banana_cart = [LineItem(‘banana‘, 30, .5),  # <5>
    ...                LineItem(‘apple‘, 10, 1.5)]
    >>> Order(joe, banana_cart, BulkItemPromo())  # <6>
    <Order total: 30.00 due: 28.50>
    >>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
    ...               for item_code in range(10)]
    >>> Order(joe, long_order, LargeOrderPromo())  # <8>
    <Order total: 10.00 due: 9.30>
    >>> Order(joe, cart, LargeOrderPromo())
    <Order total: 42.00 due: 42.00>

# END CLASSIC_STRATEGY_TESTS
"""
# BEGIN CLASSIC_STRATEGY

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:  # 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):
        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: an Abstract Base Class

    @abstractmethod
    def discount(self, order):
        """Return discount as a positive dollar amount"""


class FidelityPromo(Promotion):  # first Concrete Strategy
    """5% discount for customers with 1000 or more fidelity points"""

    def discount(self, order):
        return order.total() * .05 if order.customer.fidelity >= 1000 else 0


class BulkItemPromo(Promotion):  # second Concrete Strategy
    """10% discount for each LineItem with 20 or more units"""

    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * .1
        return discount


class LargeOrderPromo(Promotion):  # third Concrete Strategy
    """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() * .07
        return 0

# END CLASSIC_STRATEGY

使用函數實現“策略”模式

每個具體策略都是一個類,而且都只定義了一個方法,即discount。此外,策略實例沒有狀態(沒有實例屬性)。

提示 當一個類只有一個函數時,應該將其重構成一個函數。因爲函數的開銷比類小很多。

# strategy.py
# Strategy pattern -- function-based implementation

"""
# BEGIN STRATEGY_TESTS

    >>> joe = Customer(‘John Doe‘, 0)  # <1>
    >>> 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)  # <2>
    <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)  # <3>
    <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)
    <Order total: 10.00 due: 9.30>
    >>> Order(joe, cart, large_order_promo)
    <Order total: 42.00 due: 42.00>

# END STRATEGY_TESTS
"""
# BEGIN STRATEGY

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):
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion(self)  # <1>
        return self.total() - discount

    def __repr__(self):
        fmt = ‘<Order total: {:.2f} due: {:.2f}>‘
        return fmt.format(self.total(), self.due())

# <2>

def fidelity_promo(order):  # <3>
    """5% discount for customers with 1000 or more fidelity points"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


def bulk_item_promo(order):
    """10% discount for each LineItem with 20 or more units"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


def large_order_promo(order):
    """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

# END STRATEGY

選擇最佳策略:簡單的方式

promos = [fidelity_promo, bulk_item_promo, large_order_promo]  # <1>promos 列出以函數實現的各個策略。

def best_promo(order):  
    """Select best discount available
    """
    return max(promo(order) for promo in promos)

但是有些重復可能會導致不易察覺的缺陷:若想添加新的促銷策略,要定義相應的函數,還要記得把它添加到promos列表中;否則,當新促銷函數顯式地作為參數傳給 Order 時,它是可用的,但是 best_promo 不會考慮它。

找出模塊中的全部策略

promos = [globals()[name] for name in globals()  # <1>叠代 globals() 返回字典中的各個 name。
          if name.endswith(‘_promo‘)  # <2>只選擇以 _promo 結尾的名稱。
          and name != ‘best_promo‘]  # <3>過濾掉 best_promo 自身,防止無限遞歸。


def best_promo(order):
    """Select best discount available
    """
    return max(promo(order) for promo in promos)  # <4>best_promo 內部的代碼沒有變化。

這裏需要註意的地方有:

  1. 策略模式所定義的函數結尾均是_promo
  2. 不要忘記類globals()

globals()

from pprint import pprint
def fun():
    pass


dd = dict()
ll = list()

pprint(globals())

"""output
{‘__annotations__‘: {},
 ‘__builtins__‘: <module ‘builtins‘ (built-in)>,
 ‘__cached__‘: None,
 ‘__doc__‘: None,
 ‘__file__‘: ‘/home/yuanoung/Projects/fluent-python/other/test.py‘,
 ‘__loader__‘: <_frozen_importlib_external.SourceFileLoader object at 0x7f35590ca080>,
 ‘__name__‘: ‘__main__‘,
 ‘__package__‘: None,
 ‘__spec__‘: None,
 ‘dd‘: {},
 ‘fun‘: <function fun at 0x7f35590f0ea0>,
 ‘ll‘: [],
 ‘pprint‘: <function pprint at 0x7f355735b840>}
"""

重構電商例子(策略模式)