设计模式
设计模式简介
看懂UML类图和时序图
UML统一建模语言
UML类图及类图之间的关系
类关系记忆技巧
如何正确使用设计模式
优秀设计的特征
面向对象设计原则
创建型设计模式
工厂模式
抽象工厂模式
简单工厂模式
静态工厂模式(Static Factory)
单例模式
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰器模式
外观模式
享元模式
代理模式
过滤器模式
注册模式(Registry)
行为型设计模式
责任链模式
命令模式
解释器模式
中介者模式
备忘录模式
迭代器模式
观察者模式
状态模式
策略模式
模板模式
访问者模式
规格模式(Specification)
J2EE 设计模式
MVC 模式
业务代表模式
组合实体模式
数据访问对象模式(DAO模式)
前端控制器模式
拦截过滤器模式
空对象模式
服务定位器模式
传输对象模式
数据映射模式(Data Mapper)
依赖注入模式(Dependency Injection)
流接口模式(Fluent Interface)
其他模式
对象池模式(Pool)
委托模式
资源库模式(Repository)
实体属性值模式(EAV 模式)
反面模式
归纳设计模式
本文档使用 MrDoc 发布
-
+
首页
模板模式
> 模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 ![模板方法设计模式](/media/202203/2022-03-16_1432550.4872395662157424.png) 在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。 ## 问题 假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、 DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。 该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。 一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗? 还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。 ## 解决方案 模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。 让我们考虑如何在数据挖掘应用中实现上述方案。 我们可为图中的三个解析算法创建一个基类, 该类将定义调用了一系列不同文档处理步骤的模板方法。 ![模板方法定义了算法的框架](/media/202203/2022-03-16_1437330.9737013900977908.png) 模板方法将算法分解为步骤, 并允许子类重写这些步骤, 而非重写实际的模板方法。 首先, 我们将所有步骤声明为 抽象类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。 现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。 正如你所看到的那样, 我们有两种类型的步骤: 抽象步骤必须由各个子类来实现 可选步骤已有一些默认实现, 但仍可在需要时进行重写 还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。 ## 目的 模板方法模式是一种行为型的设计模式。 可能你已经见过这种模式很多次了。它是一种让抽象模板的子类「完成」一系列算法的行为策略。 众所周知的「好莱坞原则」:「不要打电话给我们,我们会打电话给你」。这个类不是由子类调用的,而是以相反的方式。怎么做?当然很抽象啦! 换而言之,它是一种非常适合框架库的算法骨架。用户只需要实现子类的一种方法,其父类便可去搞定这项工作了。 这是一种分离具体类的简单办法,且可以减少复制粘贴,这也是它常见的原因。 ## 介绍 **意图:** 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 **主要解决:** 一些方法通用,却在每一个子类都重新写了这一方法。 **何时使用:** 有一些通用的方法。 **如何解决:** 将这些通用算法抽象出来。 **关键代码:** 在抽象类实现,其他步骤在子类实现。 **应用实例:** 1. 在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2. 西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3. spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。 **优点:** 1. 封装不变部分,扩展可变部分。 2. 提取公共代码,便于维护。 3. 行为由父类控制,子类实现。 **缺点:** 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。 **使用场景:** 1. 有多个子类共有的方法,且逻辑相同。 2. 重要的、复杂的方法,可以考虑作为模板方法。 **注意事项:** 为防止恶意操作,**一般模板方法都加上 final 关键词**。 ## 结构 ![模板方法设计模式的结构](/media/202203/2022-03-16_1440060.6711786283633783.png) - 抽象类 (AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象类型, 也可以提供一些默认实现。 - 具体类 (ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身。 ### 实例 我们将创建一个定义操作的 *Game* 抽象类,其中,模板方法设置为 final,这样它就不会被重写。*Cricket* 和 *Football* 是扩展了 *Game* 的实体类,它们重写了抽象类的方法。 *TemplatePatternDemo* ,我们的演示类使用 *Game* 来演示模板模式的用法。 ![模板模式的 UML 图](/media/202203/2022-03-16_1443390.4913353768238081.png) ## 伪代码 本例中的模板方法模式为一款简单策略游戏中人工智能的不同分支提供 “框架”。 ![模板方法模式示例的结构](/media/202203/2022-03-16_1444540.020160487804851135.png) 游戏中所有的种族都有几乎同类的单位和建筑。 因此你可以在不同的种族上复用相同的 AI 结构, 同时还需要具备重写一些细节的能力。 通过这种方式, 你可以重写半兽人的 AI 使其更富攻击性, 也可以让人类侧重防守, 还可以禁止怪物建造建筑。 在游戏中新增种族需要创建新的 AI 子类, 还需要重写 AI 基类中所声明的默认方法。 ``` // 抽象类定义了一个模板方法,其中通常会包含某个由抽象原语操作调用组成的算 // 法框架。具体子类会实现这些操作,但是不会对模板方法做出修改。 class GameAI is // 模板方法定义了某个算法的框架。 method turn() is collectResources() buildStructures() buildUnits() attack() // 某些步骤可在基类中直接实现。 method collectResources() is foreach (s in this.builtStructures) do s.collect() // 某些可定义为抽象类型。 abstract method buildStructures() abstract method buildUnits() // 一个类可包含多个模板方法。 method attack() is enemy = closestEnemy() if (enemy == null) sendScouts(map.center) else sendWarriors(enemy.position) abstract method sendScouts(position) abstract method sendWarriors(position) // 具体类必须实现基类中的所有抽象操作,但是它们不能重写模板方法自身。 class OrcsAI extends GameAI is method buildStructures() is if (there are some resources) then // 建造农场,接着是谷仓,然后是要塞。 method buildUnits() is if (there are plenty of resources) then if (there are no scouts) // 建造苦工,将其加入侦查编组。 else // 建造兽族步兵,将其加入战士编组。 // ... method sendScouts(position) is if (scouts.length > 0) then // 将侦查编组送到指定位置。 method sendWarriors(position) is if (warriors.length > 5) then // 将战斗编组送到指定位置。 // 子类可以重写部分默认的操作。 class MonstersAI extends GameAI is method collectResources() is // 怪物不会采集资源。 method buildStructures() is // 怪物不会建造建筑。 method buildUnits() is // 怪物不会建造单位。 ``` ## 应用场景 - 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。 模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。 - 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。 在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。 ## 实现方式 1. 分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。 2. 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 **可用 final 最终修饰模板方法以防止子类对其进行重写**。 3. 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。 4. 可考虑在算法的**关键步骤之间添加钩子**。 5. 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。 ## 优点 - 你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。 - 你可将重复代码提取到一个超类中。 ## 缺点 - 部分客户端可能会受到算法框架的限制。 - 通过子类抑制默认步骤实现可能会导致违反里氏替换原则。 - 模板方法中的步骤越多, 其维护工作就可能会越困难。 ## 与其他模式的关系 - 工厂方法模式是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。 - 模板方法基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略模式基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。 ## 示例代码 ### TS ``` /** * The Abstract Class defines a template method that contains a skeleton of some * algorithm, composed of calls to (usually) abstract primitive operations. * * Concrete subclasses should implement these operations, but leave the template * method itself intact. */ abstract class AbstractClass { /** * The template method defines the skeleton of an algorithm. */ public templateMethod(): void { this.baseOperation1(); this.requiredOperations1(); this.baseOperation2(); this.hook1(); this.requiredOperation2(); this.baseOperation3(); this.hook2(); } /** * These operations already have implementations. */ protected baseOperation1(): void { console.log('AbstractClass says: I am doing the bulk of the work'); } protected baseOperation2(): void { console.log('AbstractClass says: But I let subclasses override some operations'); } protected baseOperation3(): void { console.log('AbstractClass says: But I am doing the bulk of the work anyway'); } /** * These operations have to be implemented in subclasses. */ protected abstract requiredOperations1(): void; protected abstract requiredOperation2(): void; /** * These are "hooks." Subclasses may override them, but it's not mandatory * since the hooks already have default (but empty) implementation. Hooks * provide additional extension points in some crucial places of the * algorithm. */ protected hook1(): void { } protected hook2(): void { } } /** * Concrete classes have to implement all abstract operations of the base class. * They can also override some operations with a default implementation. */ class ConcreteClass1 extends AbstractClass { protected requiredOperations1(): void { console.log('ConcreteClass1 says: Implemented Operation1'); } protected requiredOperation2(): void { console.log('ConcreteClass1 says: Implemented Operation2'); } } /** * Usually, concrete classes override only a fraction of base class' operations. */ class ConcreteClass2 extends AbstractClass { protected requiredOperations1(): void { console.log('ConcreteClass2 says: Implemented Operation1'); } protected requiredOperation2(): void { console.log('ConcreteClass2 says: Implemented Operation2'); } protected hook1(): void { console.log('ConcreteClass2 says: Overridden Hook1'); } } /** * The client code calls the template method to execute the algorithm. Client * code does not have to know the concrete class of an object it works with, as * long as it works with objects through the interface of their base class. */ function clientCode(abstractClass: AbstractClass) { // ... abstractClass.templateMethod(); // ... } console.log('Same client code can work with different subclasses:'); clientCode(new ConcreteClass1()); console.log(''); console.log('Same client code can work with different subclasses:'); clientCode(new ConcreteClass2()); ``` 输出: ```txt Same client code can work with different subclasses: AbstractClass says: I am doing the bulk of the work ConcreteClass1 says: Implemented Operation1 AbstractClass says: But I let subclasses override some operations ConcreteClass1 says: Implemented Operation2 AbstractClass says: But I am doing the bulk of the work anyway Same client code can work with different subclasses: AbstractClass says: I am doing the bulk of the work ConcreteClass2 says: Implemented Operation1 AbstractClass says: But I let subclasses override some operations ConcreteClass2 says: Overridden Hook1 ConcreteClass2 says: Implemented Operation2 AbstractClass says: But I am doing the bulk of the work anyway ```
追风者
2022年3月29日 20:19
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码