Python學習之路25-使用一等函數實現設計模式

《流暢的Python》筆記。

本篇主要講述Python中使用函數來實現策略模式和命令模式,最後總結出這種作法背後的思想。python

1. 重構策略模式

策略模式若是用面向對象的思想來簡單解釋的話,其實就是「多態」。父類指向子類,根據子類對同一方法的不一樣重寫,獲得不一樣結果。算法

1.1 經典的策略模式

下圖是經典的策略模式的UML類圖:編程

圖片描述

《設計模式:可複用面向對象軟件的基礎》一書這樣描述策略模式:設計模式

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

下面以一個電商打折的例子來講明策略模式,打折方案以下:微信

  • 有1000及以上積分的顧客,每一個訂單享5%優惠;
  • 同一訂單中,每類商品的數量達到20個及以上時,該類商品享10%優惠;
  • 訂單中的不一樣商品達10個及以上時,整個訂單享7%優惠。

爲此咱們須要建立5個類:app

  • Order類:訂單類,至關於上述UML圖中的Context上下文;
  • Promotion類:折扣類的父類,至關於UML圖中的Strategy策略類,實現不一樣策略的共同接口;
  • 具體策略類:FidelityPromoBulkPromoLargeOrderPromo依次對應於上述三個打折方案。

如下是經典的策略模式在Python中的實現:ide

from abc import ABC, abstractmethod
from collections import namedtuple

Customer = namedtuple("Customer", "name fidelity")

class LineItem:  # 單個商品
    def __init__(self, product, quantity, price):
        self.produce = product
        self.quantity = quantity
        self.price = price

    def total(self):
        return self.price * self.quantity

class Order:  # 訂單類,上下文
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = list(cart)  # 形參cart中的元素是LineItem
        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

class Promotion(ABC): # 策略:抽象基類
    @abstractmethod  # 抽象方法
    def discount(self, order):
        """返回折扣金額(正值)"""

class FidelityPromo(Promotion): # 第一個具體策略
    """積分1000及以上的顧客享5%"""
    def discount(self, order):
        return order.total() * 0.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() * 0.1
        return discount

class LargeOrderPromo(Promotion): # 第三個具體策略
    """訂單中的不一樣商品達到10個及以上時享7%優惠"""
    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

該類的使用示例以下:函數

>>> ann = Customer("Ann Smith", 1100)
>>> joe = Customer("John Joe", 0)
>>> cart = [LineItem("banana", 4, 0.5), LineItem("apple", 10, 1.5), 
...         LineItem("watermellon", 5, 5.0)]
>>> Order(ann, cart, FidelityPromo())  # 每次新建一個具體策略類
>>> Order(joe, cart, FidelityPromo())

1.2 Python函數重構策略模式

如今用Python函數以更少的代碼來重構上述的策略模式,去掉了抽象類Promotion,用函數代替具體的策略類:網站

# 不用導入abc模塊,去掉了Promotion抽象類;
# Customer, LineItem不變,Order類只修改due()函數;三個具體策略類改成函數
-- snip -- 
class Order:
    -- snip --
    def due(self):  # 折扣
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion(self)  # 修改成函數
        return self.total() - discount

def fidelity_promo(order):
    """積分1000及以上的顧客享5%"""
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

def bulk_item_promo(order):
    """某類商品爲20個及以上時,該類商品享10%優惠"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * 0.1
    return discount

def large_order_promo(order):
    """訂單中的不一樣商品達到10個及以上時享7%優惠"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * 0.07
    return 0

該類如今的使用示例以下:

>>> Order(ann, cart, fidelity_promo)  # 沒有實例化新的促銷對象,函數拿來即用

脫離Python語言環境,從面相對象編程來講:

1.1中的使用示例能夠看出,每次建立Order類時,都建立了一個具體策略類,即便不一樣的訂單都用的同一個策略。按理說它們應該共享同一個具體策略的實例,但實際並無。這就是策略模式的一個弊端。爲了彌補這個弊端,若是具體的策略沒有維護內部狀態,你能夠爲每一個具體策略建立一個實例,而後每次都傳入這個實例,這就是單例模式;但若是要維護內狀態,就須要將策略模式和享元模式結合使用,這又提升了代碼行數和維護成本。

在Python中則能夠用函數來避開策略模式的這些弊端:

  • 不用維護內部狀態時,咱們能夠直接用通常的函數;若是須要維護內部狀態,能夠編寫裝飾器(裝飾器也是函數);
  • 相對於編寫一個抽象類,再實現這個抽象類的接口來講,直接編寫函數更方便;
  • 函數比用戶定義的類的實例更輕量;
  • 無需去實現享元模式,每一個函數在Python編譯模塊時只會建立一次,函數自己就是可共享的對象。

1.3 自動選擇最佳策略

上述代碼中,咱們須要自行傳入打折策略,但咱們更但願的是程序自動選擇最佳打折策略。如下是咱們最能想到的一種方式:

# 在生成Order實例時,傳入一個best_promo函數,讓其自動選擇最佳策略
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # 三個打折函數的列表
def best_promo(order):
    """選擇可用的最佳策略"""
    return max(promo(order) for promo in promos)

但這樣作有一個弊端:若是要新增打折策略,不光要編寫打折函數,還得把函數手動加入到promos列表中。咱們但願程序自動識別這些具體策略。改變代碼以下:

promos = [globals()[name] for name in globals() 
          if name.endswith("_promo") and 
          name != "best_promo"] # 自動獲取當前模塊中的打折函數
def best_promo(order):
    -- snip --

在Python中,模塊也是一等對象globals()函數是標準庫提供的處理模塊的函數,它返回一個字典,表示當前全局符號表。這個符號表始終針對當前模塊(對函數或方法來講,是指定義它們的模塊,而不是調用它們的模塊)

若是咱們把各類具體策略單獨放到一個模塊中,好比放到promotions模塊中,上述代碼還可改成以下形式:

# 各具體策略單獨放到一個模塊中
import promotions, inspect
# inspect.getmembers函數用於獲取對象的屬性,第二個參數是可選的判斷條件
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
    -- snip --

其實,動態收集具體策略函數更爲顯式的一種方案是使用簡單的裝飾器,這將在下一篇中介紹。

2. 命令模式

命令模式的UML類圖以下:

圖片描述

命令模式的目的是解耦發起調用的對象(調用者,Caller)和提供實現的對象(接受者,Receiver)。實際作法就是在它們之間增長一個命令類(Command),它只有一個抽象接口execute(),具體命令類實現這個接口便可。這樣調用者就無需瞭解接受者的接口,不一樣的接受者還能夠適應不一樣的Command子類。

有人說「命令模式是回調機制的面向對象替代品」,但問題是,Python中咱們不必定須要這個替代品。具體說來,咱們能夠不爲調用者提供一個Command實例,而是給它一個函數。此時,調用者不用調用command.execute(),而是直接command()

如下是通常的命令模式代碼:

from abc import ABC, abstractmethod

class Caller:
    def __init__(self, command=None):
        self.command = command

    def action(self):
        """把對接受者的調用交給中介Command"""
        self.command.execute()

class Receiver:
    def do_something(self):
        """具體的執行命令"""
        print("I'm a receiver")

class Command(ABC):
    @abstractmethod
    def execute(self):
        """調用具體的接受者方法"""

class ConcreteCommand(Command):
    def __init__(self, receiver):
        self.receiver = receiver

    def execute(self):
        self.receiver.do_something()

if __name__ == "__main__":
    receiver = Receiver()
    command = ConcreteCommand(receiver)
    caller = Caller(command)
    caller.action()

# 結果:
I'm a receiver

直接將上述代碼改爲函數的形式,其實並不容易改寫,由於具體的命令類還保存了接收者。可是換個思路,將其改爲可調用對象,那麼代碼就能夠變成以下形式:

class Caller:
    def __init__(self, command=None):
        self.command = command

    def action(self):
        # 以前是self.command.execute()
        self.command()

class Receiver:
    def do_something(self):
        """具體的執行命令"""
        print("I'm a receiver")

class ConcreteCommand:
    def __init__(self, receiver):
        self.receiver = receiver

    def __call__(self):
        self.receiver.do_something()

if __name__ == "__main__":
    receiver = Receiver()
    command = ConcreteCommand(receiver)
    caller = Caller(command)
    caller.action()

3. 總結

看完這兩個例子,不知道你們發現了什麼類似之處了沒有:

它們都把實現單方法接口的類的實例替換成了可調用對象。畢竟,每一個Python可調用對象都實現了單方法接口,即__call__方法。

直白一點說就是,若是你定義了一個抽象類,這個類只有一個抽象方法a(),而後還要爲這個抽象類派生出一大堆具體類來重寫這個方法a(),那麼此時大可沒必要定義這個抽象類,直接將這些具體類改寫成可調用對象便可,在__call__方法中實現a()要實現的功能。

這至關於用Python中可調用對象的基類充當了咱們定義的基類,咱們便不用再定義基類;對抽象方法a()的重寫變成了對特殊方法__call__的重寫,畢竟咱們只是想要這些方法有一個相同的名字,至於叫什麼其實無所謂。


迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~

相關文章
相關標籤/搜索