Python設計模式之策略模式(15)

策略模式(The Strategy Pattern): 動態選擇算法策略。它能讓你定義一系列算法, 並將每種算法分別放入獨立的類中, 以使算法的對象能夠相互替換。
在策略模式中,我們創建表示各種策略的對象和一個行爲隨着策略對象改變而改變的 context 對象。策略對象改變 context 對象的執行算法。

1 介紹

現實中的例子:
大多數問題都可以使用多種方法來解決。以排序問題爲例,對於以一定次序把元素放入一個列表,排序算法有很多。通常來說,沒有公認最適合所有場景的算法(。一些不同的評判標準能幫助我們爲不同的場景選擇不同的排序算法,其中應該考慮的有以下幾個。

  • 需要排序的元素數量:這被稱爲輸入大小。當輸入較少時,幾乎所有排序算法的表現都很好,但對於大量輸入,只有部分算法具有不錯的性能。
  • 算法的最佳/平均/最差時間複雜度:時間複雜度是算法運行完成所花費的(大致)時間長短,不考慮係數和低階項(在算法分析中,只考慮時間複雜度函數的最高次項,不考慮低階項,也忽略最高次項的係數)。這是選擇算法的最常見標準,但這個標準並不總是那麼充分。
  • 算法的空間複雜度:空間複雜度是充分地運行一個算法所需要的(大致)物理內存量。在我們處理大數據或在嵌入式系統(通常內存有限)中工作時,這個因素非常重要。
  • 算法的穩定性:在執行一個排序算法之後,如果能保持相等值元素原來的先後相對次序,則認爲它是穩定的。
  • 算法的代碼實現複雜度:如果兩個算法具有相同的時間/空間複雜度,並且都是穩定的,那麼知道哪個算法更易於編碼實現和維護也是很重要的。

策略模式(Strategy pattern)鼓勵使用多種算法來解決一個問題,其殺手級特性是能夠在運行時透明地切換算法(客戶端代碼對變化尤感知)。因此,如果你有兩種算法,並且知道其中一種對少量輸入效果更好,另一種對大量輸入效果更好,則可以使用策略模式在運行時基於輸入數據決定使用哪種算法。

  • 意圖:定義一系列的算法,把它們一個個封裝起來, 並且使它們可相互替換。
  • 主要解決:在有多種算法相似的情況下,使用 if…else 所帶來的複雜和難以維護。
  • 何時使用:一個系統有許多許多類,而區分它們的只是他們直接的行爲。
  • 如何解決:將這些算法封裝成一個一個的類,任意地替換。
  • 關鍵代碼:實現同一個接口。
  • 優點:
    你可以在運行時切換對象內的算法。
    你可以將算法的實現和使用算法的代碼隔離開來。
    你可以使用組合來代替繼承。
    你無需對上下文進行修改就能夠引入新的策略。
  • 缺點:
    如果你的算法極少發生改變, 那麼沒有任何理由引入新的類和接口。 使用該模式只會讓程序過於複雜。
    客戶端必須知曉策略間的不同——它需要選擇合適的策略。
    許多現代編程語言支持函數類型功能, 允許你在一組匿名函數中實現不同版本的算法。 這樣, 你使用這些函數的方式就和使用策略對象時完全相同, 無需藉助額外的類和接口來保持代碼簡潔。
  • 注意事項:如果一個系統的策略多於四個,就需要考慮使用混合模式,解決策略類膨脹的問題。

2 適用場景

當你想使用對象中各種不同的算法變體, 並希望能在運行時切換算法時, 可使用策略模式。
策略模式讓你能夠將對象關聯至可以不同方式執行特定子任務的不同子對象, 從而以間接方式在運行時更改對象行爲。
當你有許多僅在執行某些行爲時略有不同的相似類時, 可使用策略模式。
策略模式讓你能將不同行爲抽取到一個獨立類層次結構中, 並將原始類組合成同一個, 從而減少重複代碼。
如果算法在邏輯的上下文中不是特別重要, 使用該模式能將類的業務邏輯與其算法實現細節隔離開來。
策略模式讓你能將各種算法的代碼、 內部數據和依賴關係與其他代碼隔離開來。 不同客戶端可通過一個簡單接口執行算法, 並能在運行時進行切換。
當類中使用了複雜條件運算符以在同一算法的不同變體中切換時, 可使用該模式。
策略模式將所有繼承自同樣接口的算法抽取到獨立類中, 因此不再需要條件語句。 原始對象並不實現所有算法的變體, 而是將執行工作委派給其中的一個獨立算法對象。

3 使用步驟

策略模式結構
在這裏插入圖片描述

實現方式

  • 從上下文類中找出修改頻率較高的算法 (也可能是用於在運行時選擇某個算法變體的複雜條件運算符)。
  • 聲明該算法所有變體的通用策略接口。
  • 將算法逐一抽取到各自的類中, 它們都必須實現策略接口。
  • 在上下文類中添加一個成員變量用於保存對於策略對象的引用。 然後提供設置器以修改該成員變量。 上下文僅可通過策略接口同策略對象進行交互, 如有需要還可定義一個接口來讓策略訪問其數據。
  • 客戶端必須將上下文類與相應策略進行關聯, 使上下文可以預期的方式完成其主要工作。

步驟

  • 策略接口聲明瞭某個算法各個不同版本間所共有的操作。上下文會使用該接口來調用有具體策略定義的算法。
  • 具體策略會在遵循策略基礎接口的情況下實現算法。該接口實現了它們在上下文
    中的互換性。
  • 上下文定義客戶端關注的接口,維護指向某個策略對象的引用。同時還提供設置器以便在運行時切換策略。上下文會將一些工作委派給策略對象,而不是自行實現不同版本的算法。
  • 客戶端代碼會選擇具體策略並將其傳遞給上下文。客戶端必須知曉策略之間的差異,才能做出正確的選擇。

4 代碼示例

概念示例

from abc import ABC, abstractmethod
from typing import List

class Stragegy(ABC):
    """策略接口聲明瞭某個算法各個不同版本間所共有的操作。上下文會使用該接口來調用有具體策略定義的算法。"""
    def altorithm(self, data: List):
        pass
class StragegyA(Stragegy):
    """具體策略會在遵循策略基礎接口的情況下實現算法。該接口實現了它們在上下文中的互換性。"""
    def algorithm(self, data: List):
        print(f"使用策略:{type(self).__name__}")
        return sorted(data)

class StragegyB(Stragegy):
    def algorithm(self, data: List):
        print(f"使用策略:{type(self).__name__}")
        return reversed(sorted(data))

class Context(object):
    """
    上下文定義了客戶端關注的接口.
    上下文會維護指向某個策略對象的引用。上下文不知曉策略的具體類。上下文必須通過策略接口來與所有策略進行交互.
    上下文通常會通過構造函數來接收策略對象,同時還提供設置器以便在運行時切換策略。
    上下文會將一些工作委派給策略對象,而不是自行實現不同版本的算法。
    """
    def __init__(self, strategy):
        self._strategy = strategy

    @property
    def strategy(self):
        """上下文會維護指向某個策略對象的引用。上下文不知曉策略的具體類。上下文必須通過策略接口來與所有策略進行交互."""
        return self._strategy

    @strategy.setter
    def strategy(self, strategy):
        """設置器以便在運行時切換策略"""
        self._strategy = strategy

    def biz_logic(self, data: List):
        """上下文會將一些工作委派給策略對象"""
        result = self._strategy.algorithm(data)
        print(','.join(result))

def main():
    data = ["a", "c", "d", "b", "e"]
    context = Context(StragegyA())
    context.biz_logic(data)
    print()

    context.strategy = StragegyB()
    context.biz_logic(data)

if __name__ == "__main__":
    main()

運行結果:

使用策略:StragegyA
a,b,c,d,e

使用策略:StragegyB
e,d,c,b,a

案例1:
商場促銷活動,同時推出多種促銷方案供給顧客選擇。一種方案是直接打9折,另一種方案是滿1000元打八折再送50元抵扣券

from abc import ABC, abstractmethod

class Strategy(ABC):

    @abstractmethod
    def discount(self, order):
        pass

class StrategyA(Strategy):

    def discount(self, order):
        return order._price * 0.1

class StrategyB(Strategy):

    def discount(self, order):
        return order._price * 0.2 + 50

class OrderContext(object):

    def __init__(self, price, discount_strategy=None):
        self._price = price
        self._strategy = discount_strategy

    @property
    def strategy(self):
        return self._strategy

    @strategy.setter
    def strategy(self, strategy):
        self._strategy = strategy

    def price_with_discount(self):
        if self._strategy:
            discount = self._strategy.discount(self)
        else:
            discount = 0
        pay = self._price - discount
        print(f'折扣策略{type(self._strategy).__name__},原價{self._price}, 折扣價: {pay}')
        return pay

def main():
    order = OrderContext(1000)
    order.price_with_discount()
    st = StrategyA()
    order.strategy = st
    order.price_with_discount()
    st2 = StrategyB()
    order.strategy = st2
    order.price_with_discount()

if __name__ == "__main__":
    main()

運行結果:

折扣策略NoneType,原價1000, 折扣價: 1000
折扣策略StrategyA,原價1000, 折扣價: 900.0
折扣策略StrategyB,原價1000, 折扣價: 750.0

5 應用案例

策略模式是一種非常通用的設計模式,可應用的場景很多。一般來說,不論何時希望動態、透明地應用不同算法,策略模式都是可行之路。這裏所說不同算法的意思是,結果相同但實現方案不同的一類算法。這意味着算法結果應該是完全一致的,但每種實現都有不同的性能和代碼複雜性(舉例來說,對比一下順序查找和二分查找)。

我們已看到Python如何使用策略模式來支持不同的排序算法。然而,策略模式並不限於排序問題,也可用於創建各種不同的資源過濾器(身份驗證、日誌記錄、數據壓縮和加密等)。

策略模式的另一個應用是創建不同的樣式表現,爲了實現可移植性(例如,不同平臺之間斷行的不同)或動態地改變數據的表現。

另一個值得一提的應用是模擬;例如模擬機器人,一些機器人比另一些更有攻擊性,一些機器人速度更快,等等。機器人行爲中的所有不同之處都可以使用不同的策略來建模(請參考網頁[t.cn/RqrBf2q])。

6 軟件例子

Python的sorted()和list.sort()函數是策略模式的例子。兩個函數都接受一個命名參數key,這個參數本質上是實現了一個排序策略的函數的名稱

7 與其他模式關係

  • 橋接模式、 狀態模式和策略模式 (在某種程度上包括適配器模式) 模式的接口非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織代碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
  • 命令模式和策略看上去很像, 因爲兩者都能通過某些行爲來參數化對象。 但是, 它們的意圖有非常大的不同。
    • 你可以使用命令來將任何操作轉換爲對象。 操作的參數將成爲對象的成員變量。 你可以通過轉換來延遲操作的執行、 將操作放入隊列、 保存歷史命令或者向遠程服務發送命令等。
    • 另一方面, 策略通常可用於描述完成某件事的不同方式, 讓你能夠在同一個上下文類中切換算法。
  • 裝飾模式可讓你更改對象的外表, 策略則讓你能夠改變其本質。
  • 模板方法模式基於繼承機制: 它允許你通過擴展子類中的部分內容來改變部分算法。 策略基於組合機制: 你可以通過對相應行爲提供不同的策略來改變對象的部分行爲。 模板方法在類層次上運作, 因此它是靜態的。 策略在對象層次上運作, 因此允許在運行時切換行爲。
  • 狀態可被視爲策略的擴展。 兩者都基於組合機制: 它們都通過將部分工作委派給 “幫手” 對象來改變其在不同情景下的行爲。 策略使得這些對象相互之間完全獨立, 它們不知道其他對象的存在。 但狀態模式沒有限制具體狀態之間的依賴, 且允許它們自行改變在不同情景下的狀態。
參考文獻

https://refactoringguru.cn/design-patterns/strategy
https://www.jianshu.com/p/91d537bb4552

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章