Python設計模式之模版模式(16)

模版模式(The Template Pattern):抽象出算法公共部分從而實現代碼複用。
模板模式中,我們可以把代碼中重複的部分抽出來作爲一個新的函數,把可變的部分作爲函數參數,從而消除代碼冗餘。一個抽象類公開定義了執行它的方法的方式/模板。它的子類可以按需要重寫方法實現,但調用將以抽象類中定義的方式進行。

1 介紹

現實生活中的例子:
工人建造房子時,設計師設計的房間基本骨架結構都是一樣的,工人只需要按照同一個模版搭建同樣的房間。毛坯房建造好以後,房主可以按照自己的喜好裝修房間,這就使得每個房間都有自己的些許不同。

  • 意圖:定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
  • 主要解決:一些方法通用,卻在每一個子類都重新寫了這一方法。
  • 何時使用:有一些通用的方法。
  • 如何解決:將這些通用算法抽象出來。
  • 關鍵代碼:在抽象類實現,其他步驟在子類實現。
  • 使用場景: 1、有多個子類共有的方法,且邏輯相同。 2、重要的、複雜的方法,可以考慮作爲模板方法。
  • 優點:1、可僅允許客戶端重寫一個大型算法中的特定部分, 使得算法其他部分修改對其所造成的影響減小。2、你可將重複代碼提取到一個超類中。
  • 缺點:1、部分客戶端可能會受到算法框架的限制。2、通過子類抑制默認步驟實現可能會導致違反里氏替換原則。3、模板方法中的步驟越多, 其維護工作就可能會越困難。

2 適用場景

當你只希望客戶端擴展某個特定算法步驟, 而不是整個算法或其結構時, 可使用模板方法模式。
模板方法將整個算法轉換爲一系列獨立的步驟, 以便子類能對其進行擴展, 同時還可讓超類中所定義的結構保持完整。
當多個類的算法除一些細微不同之外幾乎完全一樣時, 你可使用該模式。 但其後果就是, 只要算法發生變化, 你就可能需要修改所有的類。
在將算法轉換爲模板方法時, 你可將相似的實現步驟提取到超類中以去除重複代碼。 子類間各不同的代碼可繼續保留在子類中。
某超類的子類中有公有的方法,並且邏輯基本相同,可以使用模板模式
必要時可以使用鉤子方法約束其行爲。具體如本節例子;
比較複雜的算法,可以把核心算法提取出來,周邊功能在子類中實現。
例如,機器學習中的監督學習算法有很多,如決策樹、KNN、SVM等,但機器學習的流程大致相同,都包含輸入樣本、擬合(fit)、預測等過程,這樣就可以把這些過程提取出來,構造模板方法,並通過鉤子方法控制流程。

3 使用步驟

模板方法模式建議將算法分解爲一系列步驟, 然後將這些步驟改寫爲方法, 最後在 “模板方法” 中依次調用這些方法。 步驟可以是 抽象的, 也可以有一些默認的實現。 爲了能夠使用算法, 客戶端需要自行提供子類並實現所有的抽象步驟。 如有必要還需重寫一些步驟 (但這一步中不包括模板方法自身)。

模版方法模式結構
在這裏插入圖片描述
實現方式

  • 分析目標算法, 確定能否將其分解爲多個步驟。 從所有子類的角度出發, 考慮哪些步驟能夠通用, 哪些步驟各不相同。
  • 創建抽象基類並聲明一個模板方法和代表算法步驟的一系列抽象方法。 在模板方法中根據算法結構依次調用相應步驟。 可用 final最終修飾模板方法以防止子類對其進行重寫。
  • 雖然可將所有步驟全都設爲抽象類型, 但默認實現可能會給部分步驟帶來好處, 因爲子類無需實現那些方法。
  • 可考慮在算法的關鍵步驟之間添加鉤子。
  • 爲每個算法變體新建一個具體子類, 它必須實現所有的抽象步驟, 也可以重寫部分可選步驟。

步驟:
(1)分析目標算法,抽象公用部分作爲模板方法
其中通常會包含某個由抽象原語操作調用組成的算法框架。具體子類會實現這些操作,但是不會對模板方法做出修改。
(2)定義具體子類算法
具體類必須實現基類中的所有抽象操作,但是它們不能重寫模板方法自身。

4 代碼示例

概念示例

from abc import ABC, abstractmethod

class AbsAgorithm(ABC):
    """
    抽象類定義了一個模板方法,其中通常會包含某個由抽象原語操作調用組成的算法框架。
    具體子類會實現這些操作,但是不會對模板方法做出修改。
    """

    def template_skeleton(self):
        """
        定義算法的框架
        :return: None
        """
        self.base_operation1()
        self.required_operations1()
        self.base_operation2()
        self.hook1()
        self.required_operations2()

    #某些步驟可在基類中直接實現
    def base_operation1(self):
        print("算法公用方法1,初始化操作邏輯")

    def base_operation2(self):
        print("算法公用方法2,可被子算法類重寫")

    #某些可定義爲抽象類型,子算法必須實現
    @abstractmethod
    def required_operations1(self) -> None:
        pass

    @abstractmethod
    def required_operations2(self) -> None:
        pass

    #hook,子類既可以overwrite,實現各自的功能,也可以直接使用默認方法。這些hook可作爲子算法額外的擴展點

    def hook1(self):
        pass

#具體類必須實現基類中的所有抽象操作,但是它們不能重寫模板方法自身
class Agorithm1(AbsAgorithm):

    def required_operations1(self):
        print(f'子算法{type(self).__name__}執行函數required_operations1')

    def required_operations2(self):
        print(f'子算法{type(self).__name__}執行函數required_operations2')


class Agorithm2(AbsAgorithm):

    def required_operations1(self):
        print(f'子算法{type(self).__name__}執行函數required_operations1')

    def required_operations2(self):
        print(f'子算法{type(self).__name__}執行函數required_operations2')

    def hook1(self):
        print(f'子算法{type(self).__name__}擴展了功能hook1')

def client(abstract_class):
    abstract_class.template_skeleton()

if __name__ == "__main__":
    print("同樣的客戶端代碼可以使用子算法1")
    client(Agorithm1())
    print()
    print("同樣的客戶端代碼可以使用子算法2")
    client(Agorithm2())

運行結果:

同樣的客戶端代碼可以使用子算法1
算法公用方法1,初始化操作邏輯
子算法Agorithm1執行函數required_operations1
算法公用方法2,可被子算法類重寫
子算法Agorithm1執行函數required_operations2

同樣的客戶端代碼可以使用子算法2
算法公用方法1,初始化操作邏輯
子算法Agorithm2執行函數required_operations1
算法公用方法2,可被子算法類重寫
子算法Agorithm2擴展了功能hook1
子算法Agorithm2執行函數required_operations2

案例1:
投資股票是種常見的理財方式,我國股民越來越多,實時查詢股票的需求也越來越大。設計一個簡單的股票查詢客戶端。
根據股票代碼來查詢股價分爲如下幾個步驟:登錄、設置股票代碼、查詢、展示。
參考:https://www.jianshu.com/p/94046fbc8cf5

未使用模版模式代碼:

class StockQueryDevice(object):
    stock_code = None
    stock_price = None

    def login(self, usr, pwd):
        pass

    def set_code(self, code):
        self.stock_code = code

    def query_price(self):
        pass

    def show_price(self):
        pass

class WebAStockQueryDevice(StockQueryDevice):
    def login(self, usr, pwd):
        if usr == "stockA" and pwd == "pwdA":
            print(f"{type(self).__name__}:登陸成功 ... user:%s pwd:%s" % (usr, pwd))
            return True
        else:
            print(f"{type(self).__name__}:登陸失敗... user:%s pwd:%s" % (usr, pwd))
            return False

    def query_price(self):
        self.stock_price = 20.00
        print(f"查詢股票{self.stock_code}股價")

    def show_price(self):
        print(f"查詢到股票{self.stock_code}股價爲:{self.stock_price}")


class WebBStockQueryDevice(StockQueryDevice):
    def login(self, usr, pwd):
        if usr == "stockB" and pwd == "pwdB":
            print(f"{type(self).__name__}:登陸成功 ... user:%s pwd:%s" % (usr, pwd))
            return True
        else:
            print(f"{type(self).__name__}:登陸失敗... user:%s pwd:%s" % (usr, pwd))
            return False

    def query_price(self):
        self.stock_price = 20.00
        print(f"查詢{self.stock_code}股價")

    def show_price(self):
        print(f"查詢到{self.stock_code}股價爲:{self.stock_price}")

def main():
    web_a_query = WebAStockQueryDevice()
    web_a_query.login('stockA', "pwdA")
    web_a_query.set_code('12345')
    web_a_query.query_price()
    web_a_query.show_price()

if __name__ == "__main__":
    main()

運行結果:

WebAStockQueryDevice:登陸成功 ... user:stockA pwd:pwdA
查詢股票12345股價
查詢到股票12345股價爲:20.0

每次操作,都會調用登錄,設置代碼,查詢,展示這幾步,是不是有些繁瑣?既然有些繁瑣,何不將這幾步過程封裝成一個接口。由於各個子類中的操作過程基本滿足這個流程,所以這個方法可以寫在父類中:

class StockQueryDevice(object):
    stock_code = None
    stock_price = None

    def login(self, usr, pwd):
        pass

    def set_code(self, code):
        self.stock_code = code

    def query_price(self):
        pass

    def show_price(self):
        pass

    def query_operation(self, usr, pwd, code):
        self.login(usr, pwd):
        self.set_code(code)
        self.query_price()
        self.show_price()
        return True

這樣,在業務場景中,client端不需要了解調用邏輯,就能獲取業務信息。
同時,當對某些公用算法邏輯部分需要做額外處理,例如添加登陸失敗校驗只需要更改父類中的相關算法。

    def query_operation(self, usr, pwd, code):
        if not self.login(usr, pwd):
            return False
        self.set_code(code)
        self.query_price()
        self.show_price()
        return True

使用模版模式完整代碼:

class StockQueryDevice(object):
    stock_code = None
    stock_price = None

    def login(self, usr, pwd):
        pass

    def set_code(self, code):
        self.stock_code = code

    def query_price(self):
        pass

    def show_price(self):
        pass

    def query_operation(self, usr, pwd, code):
        if not self.login(usr, pwd):
            return False
        self.set_code(code)
        self.query_price()
        self.show_price()
        return True


class WebAStockQueryDevice(StockQueryDevice):
    def login(self, usr, pwd):
        if usr == "stockA" and pwd == "pwdA":
            print(f"{type(self).__name__}:登陸成功 ... user:%s pwd:%s" % (usr, pwd))
            return True
        else:
            print(f"{type(self).__name__}:登陸失敗... user:%s pwd:%s" % (usr, pwd))
            return False

    def query_price(self):
        self.stock_price = 20.00
        print(f"查詢股票{self.stock_code}股價")

    def show_price(self):
        print(f"查詢到股票{self.stock_code}股價爲:{self.stock_price}")


class WebBStockQueryDevice(StockQueryDevice):
    def login(self, usr, pwd):
        if usr == "stockB" and pwd == "pwdB":
            print(f"{type(self).__name__}:登陸成功 ... user:%s pwd:%s" % (usr, pwd))
            return True
        else:
            print(f"{type(self).__name__}:登陸失敗... user:%s pwd:%s" % (usr, pwd))
            return False

    def query_price(self):
        self.stock_price = 20.00
        print(f"查詢{self.stock_code}股價")

    def show_price(self):
        print(f"查詢到{self.stock_code}股價爲:{self.stock_price}")

# def main():
#     web_a_query = WebAStockQueryDevice()
#     web_a_query.login('stockA', "pwdA")
#     web_a_query.set_code('12345')
#     web_a_query.query_price()
#     web_a_query.show_price()

def main():
    web_a_query = WebAStockQueryDevice()
    web_a_query.query_operation('stockA', 'pwdA', '1234567')

    web_b_query = WebBStockQueryDevice()
    web_b_query.query_operation('stockA', 'pwdA', '1234567')

if __name__ == "__main__":
    main()

運行結果:

WebAStockQueryDevice:登陸成功 ... user:stockA pwd:pwdA
查詢股票1234567股價
查詢到股票1234567股價爲:20.0
WebBStockQueryDevice:登陸失敗... user:stockA pwd:pwdA

案例2:
樹狀圖遍歷,我們期望的算法輸出是從Frankfurt到Nurnberg的路徑時訪問過的城市列表。
在這裏插入圖片描述
未使用模版模式代碼:

#廣度遍歷
def bfs(graph, start, end):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
            path.append(current)
            if current == end:
                print(path)
                return (True, path)
            # 兩個頂點不相連,則跳過
            if current not in graph:
                continue
        visited = visited + graph[current]
    return (False, path)

#深度遍歷
def dfs(graph, start, end):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
            path.append(current)
            if current == end:
                print(path)
                return (True, path)
            # 兩個頂點不相連,則跳過
            if current not in graph:
                continue
        visited = graph[current] + visited
    return (False, path)

def main():
    """
    爲了簡化,假設該圖是有向的。這意味着只能朝一個方向移動,我們可以檢測如何從Frankfurt到Mannheim,而不是另一個方向。
    可以使用列表的字典結構來表示這個有向圖。每個城市是字典中的一個鍵,列表的內容是從該城市始發的所有可能H的地。
    葉子頂點的城市(例如,Erfurt)使用一個空列表即可(無目的地)。
    """
    graph = {
        'Frankfurt':  ['Mannheim', 'Wurzburg', 'Kassel'],
        'Mannheim':   ['Karlsruhe'],
        'Karlsruhe':  ['Augsburg'],
        'Augsburg':   ['Munchen'],
        'Wurzburg':   ['Erfurt', 'Nurnberg'],
        'Nurnberg':   ['Stuttgart', 'Munchen'],
        'Kassel':     ['Munchen'],
        'Erfurt':     [],
        'Stuttgart':  [],
        'Munchen':    []
        }

    bfs_path = bfs(graph, 'Frankfurt', 'Nurnberg')
    dfs_path = dfs(graph, 'Frankfurt', 'Nurnberg')
    print('bfs Frankfurt-Nurnberg: {}'.format(bfs_path[1] if bfs_path[0] else 'Not found'))
    print('dfs Frankfurt-Nurnberg: {}'.format(dfs_path[1] if dfs_path[0] else 'Not found'))

    bfs_nopath = bfs(graph, 'Wurzburg', 'Kassel')
    print('bfs Wurzburg-Kassel: {}'.format(bfs_nopath[1] if bfs_nopath[0] else 'Not found'))
    dfs_nopath = dfs(graph, 'Wurzburg', 'Kassel')
    print('dfs Wurzburg-Kassel: {}'.format(dfs_nopath[1] if dfs_nopath[0] else 'Not found'))

if __name__ == "__main__":
    main()

運行結果:

['Frankfurt', 'Mannheim', 'Wurzburg', 'Kassel', 'Karlsruhe', 'Erfurt', 'Nurnberg']
['Frankfurt', 'Mannheim', 'Karlsruhe', 'Augsburg', 'Munchen', 'Wurzburg', 'Erfurt', 'Nurnberg']
bfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Wurzburg', 'Kassel', 'Karlsruhe', 'Erfurt', 'Nurnberg']
dfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Karlsruhe', 'Augsburg', 'Munchen', 'Wurzburg', 'Erfurt', 'Nurnberg']
bfs Wurzburg-Kassel: Not found
dfs Wurzburg-Kassel: Not found

我們注意到:兩個算法之間僅有一處不同,但其餘代碼都寫了兩遍。
這個問題可以通過模板設計模式(Template design pattern)來解決。這個模式關注的是消除代碼冗餘,其思想是我們應該尤需改變算法結構就能重新定義一個算法的某些部分。爲了避免重複而進行必要的重構。

(1)使用模版模式,我們首先需要找出算法相同的部分,提取出來作爲模版。
將bfs()和dfs()函數相同的部分,提取出來命名爲traverse()函數

def traverse(graph, start, end):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
            path.append(current)
            if current == end:
                return (True, path)
            # 兩個頂點不相連,則跳過
            if current not in graph:
                continue
        ##TODO,需要能有一個參數控制此處的子算法
    return (False, path)

(2)bfs()和dfs()函數不同的部分,引入有一個通用的額外參數,控制子算法實現
我們加入了一個action參數。該參數是一個“知道”如何延伸路徑的函數。根據要使用的算法,我們可以傳遞extend_bfs_path()或extend_dfs_path()作爲目標動作。

def traverse(graph, start, end, action):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
            path.append(current)
            if current == end:
                return (True, path)
            # 兩個頂點不相連,則跳過
            if current not in graph:
                continue
        visited = action(visited, graph[current])
    return (False, path)

def extend_bfs_path(visited, current):
    return visited + current

def extend_dfs_path(visited, current):
    return current + visited

(3)改造client代碼

def main():
    """
    爲了簡化,假設該圖是有向的。這意味着只能朝一個方向移動,我們可以檢測如何從Frankfurt到Mannheim,而不是另一個方向。
    可以使用列表的字典結構來表示這個有向圖。每個城市是字典中的一個鍵,列表的內容是從該城市始發的所有可能H的地。
    葉子頂點的城市(例如,Erfurt)使用一個空列表即可(無目的地)。

    """
    graph = {
        'Frankfurt':  ['Mannheim', 'Wurzburg', 'Kassel'],
        'Mannheim':   ['Karlsruhe'],
        'Karlsruhe':  ['Augsburg'],
        'Augsburg':   ['Munchen'],
        'Wurzburg':   ['Erfurt', 'Nurnberg'],
        'Nurnberg':   ['Stuttgart', 'Munchen'],
        'Kassel':     ['Munchen'],
        'Erfurt':     [],
        'Stuttgart':  [],
        'Munchen':    []
        }

    bfs_path = traverse(graph, 'Frankfurt', 'Nurnberg', extend_bfs_path)
    dfs_path = traverse(graph, 'Frankfurt', 'Nurnberg', extend_dfs_path)
    print('bfs Frankfurt-Nurnberg: {}'.format(bfs_path[1] if bfs_path[0] else 'Not found'))
    print('dfs Frankfurt-Nurnberg: {}'.format(dfs_path[1] if dfs_path[0] else 'Not found'))

運行結果:

bfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Wurzburg', 'Kassel', 'Karlsruhe', 'Erfurt', 'Nurnberg']
dfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Karlsruhe', 'Augsburg', 'Munchen', 'Wurzburg', 'Erfurt', 'Nurnberg']

5 應用案例

模板設計模式旨在消除代碼重複。如果我們發現結構相近的(多個)算法中有重複代碼,則可以把算法的不變(通用)部分留在一個模板方法/函數中,把易變(不同)的部分移到動作/鉤子方法/函數中。

頁碼標註是一個不錯的模板模式應用案例。一個頁碼標註算法可以分爲一個抽象(不變的)部分和一個具體(易變的)部分。不變的部分關注的是最大行號/頁號這部分內容。易變的部分則包含用於顯示某個已分頁特定頁面的頁眉和頁腳的功能(請參考網頁[t.cn/RqrBT6C,第10 頁])。

所有應用框架都利用了某種形式的模板模式。在使用框架來創建圖形化應用時,通常是繼承自一個類,並實現自定義行爲。然而,在執行自定義行爲之前,通常會調用一個模板方法,該方法實現了應用中一定相同的部分,比如繪製屏幕、處理事件循環、調整窗口大小並居中,等等(請參考[EckelPython, 第143頁])。

6 軟件例子

Python在cmd模塊中使用了模板模式,該模塊用於構建面向行的命令解釋器。具體而言,cmd.Cmd.cmdloop()實現了一個算法,持續地讀取輸入命令並將命令分發到動作方法。每次循環之前、之後做的事情以及命令解析部分始終是相同的。這也稱爲一個算法的不變部分。變化的是實際的動作方法(易變的部分),請參考網頁[t.cn/RqrBT6C,第27頁]。

Python的asyncore模塊也使用了模板模式,該模塊用於實現異步套接字服務客戶端/服務器。其中諸如asyncore.dispatcher.handle_connect_event和asyncore.dispatcher. handle_write_event()之類的方法僅包含通用代碼。要執行特定於套接字的代碼,這兩個方法會執行handle_connect()方法。注意,執行的是一個特定於套接字的handle_connect(),不是asyncore.dispatcher.handle_connect()。後者僅包含一條警告。

7 與其他模式關係

  • 工廠方法模式是模板方法模式的一種特殊形式。 同時, 工廠方法可以作爲一個大型模板方法中的一個步驟。

  • 模板方法基於繼承機制: 它允許你通過擴展子類中的部分內容來改變部分算法。 策略模式基於組合機制: 你可以通過對相應行爲提供不同的策略來改變對象的部分行爲。 模板方法在類層次上運作, 因此它是靜態的。 策略在對象層次上運作, 因此允許在運行時切換行爲。

參考文獻

https://refactoringguru.cn/design-patterns/template-method
https://www.jianshu.com/p/94046fbc8cf5
https://www.jianshu.com/p/a8da7ead7c76

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