Loading...

设计模式-模板方法模式

前言

在 Java 开发中,设计模式是常用的一种编码方式。合理使用设计模式,可以帮助开发人员更快地写出可靠、容易维护的代码。本期继续更新设计模式系列,总共 23 种设计模式会用一篇一篇文章来讲解,相关代码已经开源至 Gitee点击跳转 上一篇《Java设计模式(20)策略模式》介绍了如何把多种处理方式封装成独立策略,并在运行时选择其中一种。本文是这个系列的第二十一篇,继续介绍行为型设计模式中的模板方法模式

模板方法模式

模板方法模式是一种行为型设计模式。它在父类中定义一个完整流程,把流程中需要变化的步骤交给子类实现。不同子类可以有不同做法,但整体执行顺序保持不变。

以启动游戏为例,不同游戏的画面、资源和玩法差别很大,启动过程却通常有一个大致固定的顺序:先初始化资源,等待初始化完成,然后正式开始。如果让每个游戏类自己组织这些步骤,很容易有人漏掉一步,或者把调用顺序写乱。

模板方法模式会把这个顺序写在抽象父类中。子类只负责实现初始化和启动的具体内容,调用方始终执行父类提供的模板方法。流程由父类控制,细节由子类补充。这里说的“模板”不是文档模板或代码生成模板,而是一套已经排好顺序的方法调用骨架。

使用场景

  • 多个业务流程的整体步骤相同,只有部分步骤的实现不同,例如文件导入、数据导出和报表生成。
  • 希望把公共流程集中到父类,避免多个子类重复编排相同的执行顺序。
  • 流程顺序不能被子类随意修改,但允许子类定制其中的处理细节。
  • 框架负责控制调用过程,业务代码只需要实现扩展点,例如生命周期回调和任务执行器

代码实现

下面使用项目中的游戏启动示例。Game 是抽象父类,它定义游戏执行骨架;ConcreteClass 继承 Game,并完成每一个步骤的具体逻辑。

先定义抽象类 Game。initialize、initializedstart 是需要子类实现的步骤,play 是模板方法,负责按照固定顺序调用它们。

/**
 * 游戏执行骨架
 * @author Jensen
 * @date 2024-01-19
 */
public abstract class Game {
    // 抽象方法,由子类实现
    public abstract void initialize();
    public abstract void initialized();
    public abstract void start();

    // 模板方法,定义算法流程
    public final void play() {
        initialize();
        initialized();
        start();
    }
}

play 方法使用 final 修饰,子类不能重写它。这样可以保证所有游戏都按照初始化、初始化完成、开始游戏的顺序运行。子类可以改变每一步做什么,但不能绕开父类重新安排流程。

接着创建具体实现 ConcreteClass。它继承 Game,并分别实现三个抽象方法。

/**
 * 游戏模板执行的具体细节
 * @author Jensen
 * @date 2024-01-19
 */
public class ConcreteClass extends Game {
    @Override
    public void initialize() {
        System.out.println("游戏初始化中...");
    }
    @Override
    public void initialized() {
        System.out.println("游戏初始化完成");
    }
    @Override
    public void start() {
        System.out.println("游戏开始");
    }

解释

  • 抽象模板类(Game):保存完整的游戏启动流程。它既定义了需要子类实现的步骤,也定义了调用这些步骤的模板方法 play。
  • 抽象步骤(initialize、initialized、start):这些方法只有定义,没有具体实现。每个游戏子类可以根据自己的需要完成它们。
  • 模板方法(play):按照固定顺序调用三个步骤。它使用 final 修饰,目的是防止子类修改流程骨架。
  • 具体实现类(ConcreteClass):继承 Game,负责填充每个步骤的具体行为。它不需要重写 play,也不用关心三个步骤应该按什么顺序调用。

这套代码的调用方向和普通工具类不太一样。调用方执行的是 Game 中的 playplay 再回过头调用子类实现的方法。流程掌握在父类手里,子类只在指定位置补充代码,这也是框架中常见的扩展方式。

测试

System.out.println("------------------------------模板方法模式-----------------------------");
Game game = new ConcreteClass();
game.play();

输出结果


------------------------------模板方法模式-----------------------------
游戏初始化中...
游戏初始化完成
游戏开始

测试代码只调用了一次 play,三个步骤就按照 Game 中规定的顺序依次执行。虽然变量类型是 Game,实际步骤来自 ConcreteClass。以后增加另一款游戏时,可以再创建一个 Game 子类,实现自己的初始化和启动逻辑,调用方式仍然是 game.play()。

模板方法中的钩子方法

项目里的示例全部使用抽象方法,子类必须实现每一个步骤。实际开发中,模板类还可以提供带默认实现的方法,这类方法常被称为钩子方法。

钩子方法通常不是必填项。子类不重写时使用父类默认逻辑,需要定制时再覆盖。例如模板方法可以调用 needLoadCache 判断是否加载缓存,父类默认返回 true,某个子类可以重写并返回 false。这样既能保留固定流程,也能为少量差异留出位置。

钩子不宜太多。如果一个模板类到处都是开关和可选步骤,执行流程会变得难以判断,此时拆分职责或改用组合方式往往更清楚。

模板方法模式和策略模式的区别

模板方法模式通过继承复用流程。父类固定算法骨架,子类重写其中几个步骤。调用方通常只执行模板方法,不负责挑选每一个步骤。

策略模式通过组合替换整套算法。调用方或上下文对象持有策略接口,可以在运行时换成另一个实现。上一篇的旅游计划中,StrategyMap 根据编号选择 MondaySunday,各个策略之间没有共同的执行骨架。

简单来说,模板方法适合“步骤顺序一样,部分步骤不同”的场景;策略模式适合“目标一样,但整套做法可以替换”的场景。一个依赖继承,一个更偏向组合。

模板方法模式的优缺点

模板方法模式能把重复流程放进父类,子类只处理有差异的步骤。流程顺序集中在一个地方,修改公共步骤时不需要逐个调整子类。模板方法使用 final 后,还能避免子类无意间破坏执行顺序。

它也有继承带来的限制。子类会依赖父类结构,父类增加抽象方法时,已有子类都要跟着修改。流程差异越来越大时,父类中的模板方法可能塞进很多条件判断,继承关系也会变得勉强。此时继续套用模板方法,代码未必比直接拆成独立服务更好理解。

在 Java 和 Spring 生态中,模板方法并不少见。很多框架会负责资源获取、异常处理和资源释放,只把真正的业务操作留给回调或子类。开发者看到的代码很短,是因为通用流程已经被模板封装好了。

结语

模板方法模式把稳定的流程放在父类,把容易变化的步骤交给子类。本文的游戏示例中,Game.play() 固定了初始化、初始化完成和开始游戏的顺序,ConcreteClass 只实现每一步的内容。

当多个流程看起来很像,重复的不只是几行代码,而是整套执行顺序时,可以考虑模板方法模式。它适合流程明确、扩展点也明确的场景。如果子类之间几乎没有共同步骤,或者算法需要在运行时频繁切换,继承就不是最合适的选择。

0

回到顶部