Python設計模式知多少


設計模式

設計模式是前輩們經過相當長的一段時間的試驗和錯誤總結出來的最佳實踐。我找到的資料列舉了以下這些設計模式:工廠模式、抽象工廠模式、單例模式、建造者模式、原型模式、適配器模式、橋接模式、過濾器模式、組合模式、裝飾器模式、外觀模式、享元模式、代理模式、責任鏈模式、命令模式、解釋器模式、迭代器模式、中介者模式、備忘錄模式、觀察者模式、狀態模式、空對象模式、策略模式、模板模式、訪問者模式、MVC模式、業務代表模式、組合實體模式、數據訪問對象模式、前端控制器模式、攔截過濾器模式、服務定位器模式、傳輸對象模式,共33種

這些設計模式在純面向對象編程語言中使用最多。Python擁有一等函數,既不需要使用某些設計模式,也減少了某些設計模式樣板代碼。本文將使用一等函數實現策略模式和命令模式,研究Python代碼是如何簡化的。

策略模式

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

經典實現

示例,根據客戶的屬性或訂單中的商品計算折扣,規則如下:

  • 有1000或以上積分的客戶,每個訂單享5%折扣。
  • 同一訂單中,單個商品的數量達到20個或以上,享10%折扣。
  • 訂單中的不同商品達到10個或以上,享7%折扣。

這很適合用策略模式來做,UML類圖設計如下:

image-20210419085600095

  • 上下文,集成算法的類,圖中Order會根據不同的算法計算折扣。
  • 策略,實現不同算法的組件的共同接口,圖中Promotion是個抽象類。
  • 具體策略,策略的具體子類,圖中FidelityPromo、BulkItemPromo、LargeOrderPromo分別對應上面3條計算折扣規則。

代碼實現如下:

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

實現策略模式的關鍵代碼是Promotion類,它是一個抽象基類,通過繼承abc.ABC來定義。

測試下這段代碼:

>>> joe = Customer('John Doe', 0)  # 顧客joe積分0
>>> ann = Customer('Ann Smith', 1100)  # 顧客ann積分1100

# 測試第一條折扣規則
>>> cart = [LineItem('banana', 4, .5),  # 3類商品
...         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),  # 商品數量超過20
...                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) # 10類不同商品
...               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>

函數實現

現在開始使用Python函數改寫代碼。觀察上文代碼可以發現每個具體策略是一個類,類里面只有一個方法:discount(),並且沒有屬性。看起來就像是普通的函數。改造如下:

image-20210421132250401

最關鍵的是,刪除了抽象類。測試一下,函數拿來即用的美妙體驗:

>>> 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)  # 直接傳函數名
<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)  # 直接傳函數名
<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>

函數的意義體現在:

image-20210421133340833

可以得出結論:普通函數比只有一個方法的類使用起來更簡單

選擇最佳策略

繼續看另外一個問題,從具體策略中選擇最佳策略,本文示例就是要選擇優惠最多的折扣,代碼實現如下:

promos = [fidelity_promo, bulk_item_promo, large_order_promo]

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

promos列表包含了三個具體策略。best_promo()函數先使用生成器表達式計算每個策略的折扣,再使用max()函數返回最大折扣。

測試一下:

>>> 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>

沒有問題。但是存在一個隱藏缺陷:如果想要添加新的促銷策略,那么要定義相應函數並添加到promos列表中。

添加新策略

接下來針對這個缺陷進行優化。

方法一

借助globals()函數自動找到其他可用的*_promo函數:

promos = [globals()[name] for name in globals()
            if name.endswith('_promo')
            and name != 'best_promo']

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

globals()返回一個字典,表示當前的全局符號表。這個符號表始終針對當前模塊。對函數或方法來說,是指定義它們的模塊,而不是調用它們的模塊。

方法二

通過函數內省自動查找promotions模塊中的所有函數作為策略函數(要求promotions模塊中只能包含策略函數,不能包含其他函數):

promos = [func for name, func in
                inspect.getmembers(promotions, inspect.isfunction)]

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

inspect.getmembers()的第一個參數是目標模塊(promotions模塊),第二個參數是判斷條件(只查找模塊中的函數)。

方法三

裝飾器,這個方法更優雅,在下篇文章講到裝飾器時,再給出代碼實現。

命令模式

命令模式的目的是解耦調用操作的對象(調用者)和提供實現的對象(接收者)。

示例,菜單驅動文本編輯器,調用者是菜單,接收者是被編輯的文檔。

UML類圖設計如下:

image-20210514090207203

命令模式的做法是在調用者和接收者之間放一個Command對象,讓它實現只有一個execute()方法的接口,調用接收者中的方法執行具體命令。這樣調用者Menu不需要了解接收者Document的接口。並且可以添加Command子類擴展多個不同的接收者。

使用一等函數對命令模式的優化思路:不為調用者提供一個Command對象,而是給它一個函數,調用者不用調command.execute(),直接調command()即可。這和策略模式是類似的,把實現單方法接口的類的實例替換成可調用對象

注意,圖中的MacroCommand是宏命令,可能保存一系列命令,它的execute()方法會在各個命令上調用相同的方法,在使用一等函數函數時,可以實現成定義了__call__方法的類:

class MacroCommand:
    "一個執行一組命令的命令"
    
    def __init__(self, commands):
        self.commands = list(commands)
        
    def __call__(self):
        for command in self.commands:
            command()

畢竟,__call__使得每個Python可調用對象都實現了單方法接口。

小結

本文簡單列舉了33種設計模式,從兩個經典的設計模式,策略模式和命令模式入手,介紹設計模式在Python中是如何實現的,借助函數是一等對象的這一特性,大大簡化了代碼。在此基礎上,還能更Pythonic一點,那就是用函數裝飾器和閉包。

參考資料:

《流暢的Python》

https://www.runoob.com/design-pattern/design-pattern-tutorial.html

https://blog.csdn.net/xldmx/article/details/112337759

https://github.com/fluentpython/example-code/tree/master/06-dp-1class-func


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM