模板方法模式¶
分类:行为型模式。对应《Head First 设计模式》第 8 章;原来混在命令模式页面中的模板方法、钩子和好莱坞原则摘记整理于此。
类结构¶
classDiagram
direction TB
class AbstractClass {
<<abstract>>
+templateMethod()
#step1()*
#step2()*
#hook()
}
class ConcreteClass {
#step1()
#step2()
#hook()
}
AbstractClass <|-- ConcreteClass
note for AbstractClass "templateMethod 编排 step1、step2 和 hook 的调用"
角色定义¶
| 角色 | 定义 |
|---|---|
| AbstractClass(抽象类) | 在模板方法中固定算法骨架,声明需要子类完成的抽象步骤,并为钩子提供默认实现。 |
| ConcreteClass(具体类) | 实现抽象步骤,按需重写钩子。图中展示重写钩子的情况,子类也可以沿用默认实现。 |
定义¶
模板方法模式在一个方法中定义算法的骨架,将某些步骤延迟到子类实现,使子类能够在保持整体算法结构的前提下重新定义这些步骤。
例如,制作茶和咖啡都要经历“烧水 → 冲泡 → 倒入杯子 → 添加配料”。父类固定步骤顺序,子类分别实现冲泡与添加配料。需要保护流程顺序时,可以把模板方法声明为 final。
使用钩子¶
钩子是父类提供默认实现、允许子类选择性重写的方法。它适合表达可选步骤或条件,例如由 customerWantsCondiments() 决定是否添加配料。
必须由子类提供的步骤通常声明为抽象方法;已有合理默认行为、只需在部分子类中改变的步骤可以设计为钩子。
好莱坞原则¶
好莱坞原则:别调用我们,我们会调用你。
在模板方法中,高层流程掌握执行顺序,并在适当时机调用子类提供的具体步骤。子类提供变化点,通常不负责重新编排整个流程。
用模板方法排序¶
排序可以用来理解这种职责划分:排序流程控制比较、交换与迭代,而元素比较规则由具体类型或比较器提供。具体排序 API 的扩展机制需要结合实现分析,不能仅凭出现回调就判定为模板方法模式。
与工厂方法、策略的关系¶
- 工厂方法关注创建对象的可变步骤。披萨店的
orderPizza()固定制作流程,调用可被子类实现的createPizza();其中同时包含模板方法与工厂方法的思路。 - 策略通常通过组合委托给可替换的算法对象;模板方法通常通过继承让子类实现流程中的变化步骤。
Head First 示例:咖啡、茶与调料钩子¶
第 8 章把制作咖啡和茶的共同流程整理到父类:烧水、冲泡、倒入杯子,再按顾客意愿加入调料。下面选择作者 2020 年第二版配套源码中的带钩子版本。
prepareRecipe() 在源码中是 final 方法,固定步骤顺序;brew()、addCondiments() 由子类实现。customerWantsCondiments() 有默认实现,两种具体饮料在这里读取顾客选择。图中省略输入处理辅助方法。
| 示例类型 | 模式角色 | 职责 |
|---|---|---|
| CaffeineBeverageWithHook | 抽象类 | 提供制作流程、共有步骤和默认钩子,并声明两个可变步骤。 |
| CoffeeWithHook | 具体类 | 实现过滤冲泡、加入糖与牛奶,以及是否加调料的询问。 |
| TeaWithHook | 具体类 | 实现浸泡茶叶、加入柠檬,以及是否加调料的询问。 |
示例 UML¶
classDiagram
direction TB
class CaffeineBeverageWithHook {
<<abstract>>
prepareRecipe()
boilWater()
brew()*
pourInCup()
addCondiments()*
customerWantsCondiments() boolean
}
class CoffeeWithHook {
+brew()
+addCondiments()
+customerWantsCondiments() boolean
}
class TeaWithHook {
+brew()
+addCondiments()
+customerWantsCondiments() boolean
}
CaffeineBeverageWithHook <|-- CoffeeWithHook
CaffeineBeverageWithHook <|-- TeaWithHook
note for CaffeineBeverageWithHook "prepareRecipe 在源码中声明为 final"
协作过程¶
- 测试入口创建咖啡或茶对象,统一调用继承的
prepareRecipe()。 - 父类先执行烧水,随后动态调用具体饮料的
brew()。 - 倒入杯子后,调用
customerWantsCondiments()钩子。 - 仅在钩子返回
true时调用子类的addCondiments();流程顺序仍由父类掌握。
配套源码(2020 年第二版,固定版本):