Python設計模式之策略模式

前言

設計模式是我們實際應用開發中必不可缺的,對設計模式的理解有助于我們寫出可讀性和擴展更高的應用程序。雖然設計模式與語言無關,但并不意味著每一個模式都能在任何語言中使用,所以有必要去針對語言的特性去做了解。設計模式特別是對于java語言而言,已經有過非常多的大牛寫過,所以這里我就不重復了。對于Python來說就相對要少很多,特別是python語言具有很多高級的特性,而不需要了解這些照樣能滿足開發中的很多需求,所以很多人往往忽視了這些,這里我們來在Pythonic中來感受一下設計模式。

1.介紹

策略模式也是常見的設計模式之一,它是指對一系列的算法定義,并將每一個算法封裝起來,而且使它們還可以相互替換。策略模式讓算法獨立于使用它的客戶而獨立變化。
這是比較官方的說法,看著明顯的一股比較抽象的感覺,通俗來講就是針對一個問題而定義出一個解決的模板,這個模板就是具體的策略,每個策略都是按照這個模板來的。這種情況下我們有新的策略時就可以直接按照模板來寫,而不會影響之前已經定義好的策略。

2.具體實例

這里我用的《流暢的Python》中的實例,剛好雙11過去不久,相信許多小伙伴也是掏空了腰包,哈哈。那這里就以電商領域的根據客戶的屬性或訂單中的商品數量來計算折扣的方式來進行講解,首先來看看下面這張圖。


電商大促銷

通過這張圖,相信能對策略模式的流程有個比較清晰的了解了。然后看看具體的實現過程,首先我們用namedtuple來定義一個Customer,雖然這里是說設計模式,考慮到有些小伙伴可能對Python中的具名元組不太熟悉,所以這里也簡單的說下。
namedtuple用來構建一個帶字段名的元組和一個有名字的類,這樣說可能還是有些抽象,這里來看看下面的代碼

from collections import namedtuple
City = namedtuple('City','name country provinces')

這里測試就直接如下

changsha = City('Changsha','China','Hunan')
print(changsha)

結果如下

City(name='Changsha', country='China', province='Hunan')

還可以直接調用字段名

print(changsha.name)

更多用法可以去看看官方文檔,這里重點還是講設計模式。
好了,先來看看用類實現的策略模式

# 策略設計模式實例

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:
  
  # 傳入三個參數,分別是消費者,購物清單,促銷方式
    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):
        """
        :param order:
        :return: 返回折扣金額(正值)
        """


# 第一個具體策略
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_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * .07
        return 0

這里是用類對象來實現的策略模式,每個具體策略類(折扣方式)都繼承了Promotion這個基類,因為discount()是一個抽象函數,所以繼承Promotion的子類都需要重寫discount()函數(也就是進行具體的打折信息的函數),這樣一來,就很好的實現對象之間的解耦。這里的折扣方式有兩類,一類是根據用戶的積分,一類是根據用戶所購買商品的數量。具體的折扣信息也都在代碼塊里面注釋了,這里就不重復了,接下來我們來看看具體的測試用例

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)]
print('John: ', Order(joe, cart, FidelityPromo()))
print('Ann: ', Order(ann, cart, FidelityPromo()))

這里定義了兩消費者,John初始積分為0,Ann初始積分為1100,然后商品購買了4個香蕉,10個蘋果,5個西瓜...說的都要流口水了,哈哈哈。回到正題,輸出時采用第一種折扣方式,Run一下

John:  <Order total: 42.00 due: 42.00>
Ann:  <Order total: 42.00 due: 39.90>

3.優化措施

?類變函數

上面的策略模式是使用的類對象實現的,其實我們還可以用函數對象的方法實現,看看具體的代碼

# 策略設計模式實例

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:

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


# 第一個具體策略
def fidelity_promo(order):
    """ 為積分為1000或以上的顧客提供5%的折扣 """

    return order.total() * .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() * .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() * .07
    return 0    

這種方式沒有了抽象類,并且每個策略都是函數,實現同樣的功能,代碼量更加少,并且測試的時候可以直接把促銷函數作為參數傳入,這里就不多說了。

?選擇最佳策略

細心的朋友可能觀察到,我們這樣每次對商品進行打折處理時,都需要自己選擇折扣方式,這樣數量多了就會非常的麻煩,那么有沒有辦法讓系統幫我們自動選擇呢?當然是有的,這里我們可以定義一個數組,把折扣策略的函數當作元素傳進去。

promos = [fidelity_promo,bulk_item_promo,large_order_promo]

然后定義一個函數

def best_promo(order):
    """  選擇可用的最佳折扣 """
    
    return max(promo(order) for promo in promos)

這樣一來就省了很多時間,系統幫我們自動選擇。但是仍然有一個問題,這個數組的元素需要我們手動輸入,雖然工作量小,但是對于有強迫癥的猿來說,依然是不行的,能用自動化的方式就不要用手動,所以繼續做優化。

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

這里使用了globals()函數,我們就是使用這個函數來進行全局查找以'_promo'結尾的函數,并且過濾掉best_promo函數,又一次完成了我們的自動化優化。

最后,這篇blog就到這里了,相信你我都更加了解Python中的策略模式了,這里我推薦對Python感興趣的朋友去看一下《Fluent Python》這本書,里面講述了很多的高級特性, 更加讓我們體驗到Python中的美學。
首發legend's blog

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容

  • 策略模式 大多數問題都可以使用多種方法來解決。以排序問題為例,對于以一定次序把元素放入一個列表,排序算法有很多。通...
    英武閱讀 1,845評論 0 50
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,692評論 25 708
  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 3,960評論 1 15
  • 工廠模式類似于現實生活中的工廠可以產生大量相似的商品,去做同樣的事情,實現同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,799評論 2 17
  • 最近因為待業,所以平日里的寫的東西開始天馬行空、沒有方向,想到哪里就寫到哪里。看到朋友們在寫連載小說。就心血來頭,...
    芃芃的書房閱讀 1,295評論 0 2