【设计模式】模板方法模式

模板方法模式(Template Method Pattern)详解


一、模板方法模式简介

模板方法模式(Template Method Pattern) 是一种 行为型设计模式(类行为型模式),它定义了一个算法的骨架(即"模板"),而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

你可以这样理解:

"模板方法就像一份'菜谱',规定了做菜的基本流程(洗菜 → 切菜 → 炒菜 → 调味 → 出锅),但具体的细节(比如放什么调料、炒多久)由不同的厨师(子类)来决定。"

是一种基于继承的代码复用技术。

将一些复杂流程的实现步骤封装在一系列基本方法中。

在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。

模板方法模式包含以下两个角色 :

AbstractClass(抽象类)

ConcreteClass(具体子类)


模板方法模式的实现

模板方法 (Template Method)

基本方法 (Primitive Method)

抽象方法(Abstract Method)

具体方法(Concrete Method)

钩子方法(Hook Method) :"挂钩"方法和空方法


二、解决的问题类型

模板方法模式主要用于解决以下问题:

  • 多个类实现同一个算法,但部分步骤不同:为了避免代码重复,提取公共流程。
  • 希望统一算法结构,控制子类的行为边界:防止子类修改关键流程。
  • 需要封装不变部分,扩展可变部分:提高代码复用性和可维护性。

三、使用场景

场景 示例
构建流程标准化 如构建一个Web页面:头部 → 内容 → 尾部
数据处理流程 如数据导入:连接数据库 → 读取数据 → 处理数据 → 保存结果
单元测试框架 如 JUnit 的 setUp() → testXXX() → tearDown() 流程
工厂构建流程 如创建对象的通用步骤,但具体初始化逻辑不同

一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。

各子类中公共的行为应被提取出来,并集中到一个公共父类中,以避免代码重复。

需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。


四、核心概念

  1. AbstractClass(抽象类):定义算法的骨架(模板方法),包含一些抽象操作供子类实现。
  2. ConcreteClass(具体子类):实现抽象类中定义的抽象方法,完成特定逻辑。
  3. 模板方法(Template Method):在抽象类中定义,调用一系列基本方法来完成整个算法流程。

五、实际代码案例(Java)

我们以"制作饮品"为例,演示模板方法模式的使用。比如咖啡和茶的制作流程相似,但具体步骤略有不同。

1. 定义抽象类:饮品制作者

java 复制代码
// 饮品制作模板(抽象类)
abstract class BeverageMaker {

    // 模板方法:定义制作饮品的通用流程(不可被重写)
    public final void makeBeverage() {
        boilWater();           // 煮水
        brew();                // 冲泡
        pourInCup();           // 倒入杯中
        addCondiments();       // 添加调料
    }

    // 公共方法(所有饮品都一样)
    private void boilWater() {
        System.out.println("✅ 正在煮水...");
    }

    private void pourInCup() {
        System.out.println("✅ 正在倒入杯中...");
    }

    // 抽象方法:由子类实现
    abstract void brew();           // 冲泡方式不同
    abstract void addCondiments();  // 添加的调料不同
}

2. 创建具体子类:咖啡制作者

java 复制代码
class CoffeeMaker extends BeverageMaker {
    @Override
    void brew() {
        System.out.println("☕ 正在用热水冲泡咖啡豆...");
    }

    @Override
    void addCondiments() {
        System.out.println("🥛 正在加入牛奶和糖...");
    }
}

3. 创建具体子类:茶制作者

java 复制代码
class TeaMaker extends BeverageMaker {
    @Override
    void brew() {
        System.out.println("🍵 正在用热水冲泡茶叶...");
    }

    @Override
    void addCondiments() {
        System.out.println("🍯 正在加入柠檬和蜂蜜...");
    }
}

4. 客户端测试类

java 复制代码
public class Client {
    public static void main(String[] args) {
        System.out.println("=== 制作一杯咖啡 ===");
        BeverageMaker coffee = new CoffeeMaker();
        coffee.makeBeverage();

        System.out.println("\n=== 制作一杯茶 ===");
        BeverageMaker tea = new TeaMaker();
        tea.makeBeverage();
    }
}

输出结果:

复制代码
=== 制作一杯咖啡 ===
✅ 正在煮水...
☕ 正在用热水冲泡咖啡豆...
✅ 正在倒入杯中...
🥛 正在加入牛奶和糖...

=== 制作一杯茶 ===
✅ 正在煮水...
🍵 正在用热水冲泡茶叶...
✅ 正在倒入杯中...
🍯 正在加入柠檬和蜂蜜...

六、钩子方法(Hook Method)扩展

有时我们希望子类可以选择性地执行某些步骤。这时可以使用"钩子方法"。

例如:是否需要添加调料?

java 复制代码
abstract class BeverageMakerWithHook {

    public final void makeBeverage() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {  // 钩子方法判断
            addCondiments();
        }
    }

    private void boilWater() {
        System.out.println("✅ 正在煮水...");
    }

    private void pourInCup() {
        System.out.println("✅ 正在倒入杯中...");
    }

    abstract void brew();
    abstract void addCondiments();

    // 钩子方法:默认返回 true,子类可覆盖
    boolean customerWantsCondiments() {
        return true;
    }
}

// 子类可以选择是否加料
class BlackTeaMaker extends BeverageMakerWithHook {
    @Override
    void brew() {
        System.out.println("🍵 正在冲泡红茶...");
    }

    @Override
    void addCondiments() {
        System.out.println("🍯 加入蜂蜜");
    }

    // 覆盖钩子方法:默认不加调料
    @Override
    boolean customerWantsCondiments() {
        return false;
    }
}

典型代码

钩子方法

(1) "挂钩"方法: IsXXX(),返回类型为bool类型

(2) 空方法

csharp 复制代码
......
//模板方法
public void TemplateMethod() 
{
	Open();
	Display();
	//通过钩子方法来确定某步骤是否执行
	if (IsPrint()) 
	{
	    Print();
	}
}

//钩子方法
public bool IsPrint()
{
    return true;
}
......

抽象类典型代码

csharp 复制代码
abstract class AbstractClass 
{
//模板方法
public void TemplateMethod() 
{
        PrimitiveOperation1();
        PrimitiveOperation2();
        PrimitiveOperation3();
}
//基本方法---具体方法
public void PrimitiveOperation1() 
{
    //实现代码
}
//基本方法---抽象方法
    public abstract void PrimitiveOperation2();    
//基本方法---钩子方法
public virtual void PrimitiveOperation3()   
{  }
}

具体子类典型代码

csharp 复制代码
class ConcreteClass : AbstractClass 
{
public override void PrimitiveOperation2() 
{
    //实现代码
}
public override void PrimitiveOperation3() 
{
    //实现代码
}
}

其他案例

  1. 某软件公司要为某银行的业务支撑系统开发一个利息计算模块,利息的计算流程如下:
    (1) 系统根据账号和密码验证用户信息,如果用户信息错误,则系统显示出错提示。
    (2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式)。
    (3) 系统显示利息。
    现使用模板方法模式设计该利息计算模块。
  1. 某软件公司要为销售管理系统提供一个数据图表显示功能,该功能的实现包括以下几个步骤:
    (1) 从数据源获取数据。
    (2) 将数据转换为XML格式。
    (3) 以某种图表方式显示XML格式的数据。

该功能支持多种数据源和多种图表显示方式,但所有的图表显示操作都基于XML格式的数据,因此可能需要对数据进行转换,如果从数据源获取的数据已经是XML数据,则无须转换。

钩子方法的使用

csharp 复制代码
//DataViewer.cs
using System;
namespace TemplateMethodSample
{
    abstract class DataViewer
    {
        //抽象方法:获取数据
        public abstract void GetData();
        //具体方法:转换数据
        public void ConvertData() 
        {
             Console.WriteLine("将数据转换为XML格式。");
        }
        //抽象方法:显示数据
        public abstract void DisplayData();
        //钩子方法:判断是否为XML格式的数据
        public virtual bool IsNotXMLData()
        {
            return true;
        }
        //模板方法
        public void Process()
        {
            GetData();
            //如果不是XML格式的数据则进行数据转换
            if (IsNotXMLData())
            {
                ConvertData();
            }
            DisplayData();
        }
    }
}
  1. 银行业务办理流程
    在银行办理业务时,一般都包含几个基本步骤,首先需要取号排队,然后办理具体业务,最后需要对银行工作人员进行评分。无论具体业务是取款、存款还是转账,其基本流程都一样。现使用模板方法模式模拟银行业务办理流程
  1. 数据库操作模板

对数据库的操作一般包括连接、打开、使用、关闭等步骤,在数据库操作模板类中我们定义了connDB()、openDB()、useDB()、closeDB()四个方法分别对应这四个步骤。对于不同类型的数据库(如SQL Server和Oracle),其操作步骤都一致,只是连接数据库connDB()方法有所区别,现使用模板方法模式对其进行设计。

七、优缺点分析

优点 描述
✅ 代码复用性强 公共流程提取到父类,避免重复代码。提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为
✅ 符合开闭原则 新增功能只需添加子类,无需修改模板
✅ 控制子类扩展 关键流程不可更改,保证算法一致性。在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序
其他 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行
缺点 描述
❌ 增加类数量 每个变体都需要一个子类
❌ 违反里氏替换原则风险 子类若修改模板方法行为可能导致错误(但模板方法通常设为 final 避免)
❌ 灵活性受限 算法结构固定,难以应对流程变化大的需求

八、与策略模式对比(补充)

对比点 模板方法模式 策略模式
实现方式 继承(父类定义流程) 组合(对象持有策略)
扩展性 通过继承扩展 通过接口实现替换
运行时切换 不易动态切换流程 可在运行时更换策略
耦合度 父子类紧耦合 客户端与策略松耦合

九、最终小结

模板方法模式是一种非常实用且经典的设计模式 ,特别适用于那些流程固定但细节可变的业务场景。它通过"封装不变,扩展可变"的思想,帮助开发者构建结构清晰、易于维护的系统。

作为一名 Java 开发工程师,在开发框架、工具类、构建流程、数据处理管道等项目中,模板方法模式经常被广泛应用(如 Spring 框架中的 JdbcTemplate、RestTemplate 等都体现了这一思想)。


📌 一句话总结:

模板方法模式就像"填空题",父类出题(定义流程),子类答题(实现细节),既统一了格式,又保留了灵活性。


✅ 推荐使用场景:

  • 多个类有相似的算法流程;
  • 希望防止子类修改核心流程;
  • 需要统一处理异常、日志、资源管理等横切逻辑。

十,扩展

关于继承

模板方法模式鼓励我们恰当使用继承,此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到父类里面,而将特殊化的行为代码移到子类里面。这也进一步说明,虽然继承复用存在一些问题,但是在某些情况下还是可以给开发人员带来方便,模板方法模式就是体现继承优势的模式之一。

好莱坞原则

在模板方法模式中,子类不显式调用父类的方法,而是通过覆盖父类的方法来实现某些具体的业务逻辑,父类控制对子类的调用,这种机制被称为好莱坞原则(Hollywood Principle),好莱坞原则的定义为:"不要给我们打电话,我们会给你打电话(Don't call us, we'll call you)"。

在模板方法模式中,好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类,将某些步骤的实现写在子类中,由父类来控制整个过程。

钩子方法的使用

钩子方法的引入使得子类可以控制父类的行为。

最简单的钩子方法就是空方法,也可以在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。

比较复杂一点的钩子方法可以对其他方法进行约束,这种钩子方法通常返回一个boolean类型,即返回true或false,用来判断是否执行某一个基本方法。

相关推荐
ShyanZh12 小时前
【Python3基础】13-Python 常用设计模式
开发语言·python·设计模式
YYYing.21 小时前
【设计模式系列 (五) 】原型模式
开发语言·后端·设计模式·原型模式·c/c++
长按助力退休2 天前
设计模式实战,这 5 个最常用
设计模式
Carl_奕然2 天前
【智能体】Agent的四种设计模式之:Multi-Agent Collaboration
设计模式
Lost of 程序猿2 天前
建造者模式实战:告别“十参数构造函数“的数据导出任务
后端·设计模式·c#·asp.net
SL_staff2 天前
合规性折旧:当知识资产因主权缺位在审计中‘功能性清零’
java·设计模式·开源
Zane19942 天前
工厂方法一定比简单工厂高级?三种工厂模式到底该怎么选
设计模式
Hespethorn2 天前
设计模式是语言缺陷的补丁 —— 从工厂、单例、策略三个模式说起
设计模式
Lost of 程序猿2 天前
命令模式实战:把“下单后的动作“打包成可执行的任务
后端·设计模式·c#·asp.net·命令模式
一只旭宝2 天前
【C++复习】四种类型转换 + 常见设计模式 + Redis/MySQL(后端面试复习完整版)
c++·redis·笔记·mysql·设计模式