跳转至

模板方法模式

分类:行为型模式。对应《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"

协作过程

  1. 测试入口创建咖啡或茶对象,统一调用继承的 prepareRecipe()。
  2. 父类先执行烧水,随后动态调用具体饮料的 brew()。
  3. 倒入杯子后,调用 customerWantsCondiments() 钩子。
  4. 仅在钩子返回 true 时调用子类的 addCondiments();流程顺序仍由父类掌握。

配套源码(2020 年第二版,固定版本):

评论