模版模式(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 ==