模版模式(The Template Pattern):抽象出算法公共部分从而实现代码复用。
模板模式中,我们可以把代码中重复的部分抽出来作为一个新的函数,把可变的部分作为函数参数,从而消除代码冗余。一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

1 介绍

现实生活中的例子:
工人建造房子时,设计师设计的房间基本骨架结构都是一样的,工人只需要按照同一个模版搭建同样的房间。毛坯房建造好以后,房主可以按照自己的喜好装修房间,这就使得每个房间都有自己的些许不同。

  • 意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
  • 主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
  • 何时使用:有一些通用的方法。
  • 如何解决:将这些通用算法抽象出来。
  • 关键代码:在抽象类实现,其他步骤在子类实现。
  • 使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
  • 优点:1、可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。2、你可将重复代码提取到一个超类中。
  • 缺点:1、部分客户端可能会受到算法框架的限制。2、通过子类抑制默认步骤实现可能会导致违反里氏替换原则。3、模板方法中的步骤越多, 其维护工作就可能会越困难。

2 适用场景

当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。
当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。
某超类的子类中有公有的方法,并且逻辑基本相同,可以使用模板模式
必要时可以使用钩子方法约束其行为。具体如本节例子;
比较复杂的算法,可以把核心算法提取出来,周边功能在子类中实现。
例如,机器学习中的监督学习算法有很多,如决策树、KNN、SVM等,但机器学习的流程大致相同,都包含输入样本、拟合(fit)、预测等过程,这样就可以把这些过程提取出来,构造模板方法,并通过钩子方法控制流程。

3 使用步骤

模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。

模版方法模式结构

实现方式

  • 分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。
  • 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用 final最终修饰模板方法以防止子类对其进行重写。
  • 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。
  • 可考虑在算法的关键步骤之间添加钩子。
  • 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。

步骤:
(1)分析目标算法,抽象公用部分作为模板方法
其中通常会包含某个由抽象原语操作调用组成的算法框架。具体子类会实现这些操作,但是不会对模板方法做出修改。
(2)定义具体子类算法
具体类必须实现基类中的所有抽象操作,但是它们不能重写模板方法自身。

4 代码示例

概念示例

from abc import ABC, abstractmethodclass 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,可被子算法类重写")#某些可定义为抽象类型,子算法必须实现@abstractmethoddef required_operations1(self) -> None:pass@abstractmethoddef 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 = Nonestock_price = Nonedef login(self, usr, pwd):passdef set_code(self, code):self.stock_code = codedef query_price(self):passdef show_price(self):passclass 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 Trueelse:print(f"{type(self).__name__}:登陆失败... user:%s pwd:%s" % (usr, pwd))return Falsedef query_price(self):self.stock_price = 20.00print(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 Trueelse:print(f"{type(self).__name__}:登陆失败... user:%s pwd:%s" % (usr, pwd))return Falsedef query_price(self):self.stock_price = 20.00print(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 = Nonestock_price = Nonedef login(self, usr, pwd):passdef set_code(self, code):self.stock_code = codedef query_price(self):passdef show_price(self):passdef 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 Falseself.set_code(code)self.query_price()self.show_price()return True

使用模版模式完整代码:

class StockQueryDevice(object):stock_code = Nonestock_price = Nonedef login(self, usr, pwd):passdef set_code(self, code):self.stock_code = codedef query_price(self):passdef show_price(self):passdef query_operation(self, usr, pwd, code):if not self.login(usr, pwd):return Falseself.set_code(code)self.query_price()self.show_price()return Trueclass 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 Trueelse:print(f"{type(self).__name__}:登陆失败... user:%s pwd:%s" % (usr, pwd))return Falsedef query_price(self):self.stock_price = 20.00print(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 Trueelse:print(f"{type(self).__name__}:登陆失败... user:%s pwd:%s" % (usr, pwd))return Falsedef query_price(self):self.stock_price = 20.00print(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:continuevisited = 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:continuevisited = graph[current] + visitedreturn (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:continuevisited = action(visited, graph[current])return (False, path)def extend_bfs_path(visited, current):return visited + currentdef 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

Python设计模式之模版模式(16)相关推荐

  1. python策略模式包含角色_详解Python设计模式之策略模式

    虽然设计模式与语言无关,但这并不意味着每一个模式都能在每一门语言中使用.<设计模式:可复用面向对象软件的基础>一书中有 23 个模式,其中有 16 个在动态语言中"不见了,或者简 ...

  2. Python设计模式-装饰器模式

    Python设计模式-装饰器模式 代码基于3.5.2,代码如下; #coding:utf-8 #装饰器模式class Beverage():name = ""price = 0.0 ...

  3. Python设计模式-中介者模式

    Python设计模式-中介者模式 代码基于3.5.2,代码如下; #coding:utf-8 #中介者模式class colleague():mediator = Nonedef __init__(s ...

  4. Python设计模式-职责链模式

    Python设计模式-职责链模式 代码基于3.5.2,代码如下; #coding:utf-8 #职责链模式class Handler():def __init__(self):self.success ...

  5. Python设计模式-享元模式

    Python设计模式-享元模式 基于Python3.5.2,代码如下 #coding:utf-8class Coffee:name = ""price = 0def __init_ ...

  6. python的编程模式-Python设计模式之状态模式原理与用法详解

    本文实例讲述了Python设计模式之状态模式原理与用法.分享给大家供大家参考,具体如下: 状态模式(State Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类 ...

  7. python设计模式之建造者模式

    python设计模式之建造者模式 ​ 建造者模式的适用范围:想要创建一个由多个部分组成的对象,而且它的构成需要一步接一步的完成.只有当各个部分都完成了,这个对象才完整.建造者模式表现为复杂对象的创建与 ...

  8. java设计模式_模版模式

    2019独角兽企业重金招聘Python工程师标准>>> package com.wangbiao.test;/*** 模版模式,好比一个业务的流程一样,抽象类大致的设计了流程的步骤, ...

  9. python工厂模式 理解_浅谈Python设计模式 - 抽象工厂模式

    声明:本系列文章主要参考<精通Python设计模式>一书,并且参考一些资料,结合自己的一些看法来总结而来. 在上一篇我们对工厂模式中的普通工厂模式有了一定的了解,其实抽象工作就是 表示针对 ...

最新文章

  1. MyEclipse快捷键
  2. java拉姆达表达式事例,Java Lambda表达式详解和实例
  3. docker概念:用Dockerfile生成Image
  4. 科大星云诗社动态20220104
  5. Android ListView 的优化
  6. 【LeetCode笔记】104. 二叉树的最大深度(Java、DFS、二叉树)
  7. Juniper基础系列之一---vlan的建立
  8. 世界一流大学博士生典型科研特征
  9. [ JAVA编程 ] double类型计算精度丢失问题及解决方法
  10. python安装包的方法与图解_Python包的几种安装方法,方式
  11. linux 终止作业任务命令,linux-尝试终止进程时出错—“ kill:pid:参数必须是进程或作业ID”...
  12. x7 z8750 linux,x7-z8750 vs m3-7y30
  13. DbgView远程调试
  14. Nginx——Nginx实现服务端集群搭建
  15. F-Droid换源的坑
  16. Oracle中文乱码(中文变问号?)解决方法---简单粗暴高效
  17. 4.9 黑名单数据库的界面展示
  18. C++的explicit是什么?
  19. Excel使用攻略(1)
  20. python字符串设置字体_[4] Python字符串

热门文章

  1. ISP AWB自动白平衡包含一段简单的色温估计进行白平衡调节的程序
  2. One PUNCH Man——深度学习入门
  3. C. The Intriguing Obsession(组合数学)
  4. AE特效的理解从入门到入门
  5. 华为新员工入职培训计划曝光!你留不住员工的原因就在这! zz
  6. 使用计算机控制台方法,电脑打开控制面板的几种方法
  7. thinkpad e450 win7黑苹果macos 10.10.5(网/显/声卡驱动)安装成功
  8. 2022危险化学品生产单位安全生产管理人员考试题库及模拟考试
  9. VENC 通道属性配置参数理解
  10. Qt编译zlib完成文件压缩解压(Ubuntu18.04)