一、模板方法模式:算法骨架的艺术

模板方法模式,说白了就是——把算法的骨架定好,具体步骤留给子类去填。我做了这么多年架构,这个模式用得特别频繁。它属于行为型模式,核心思想其实很简单:父类定义一个操作中的算法框架,子类在不改变结构的情况下重写某些步骤。

举个例子你就明白了。泡茶和泡咖啡,步骤是不是很像?烧水、冲泡、倒杯、加料。但具体怎么冲泡、加什么料,又不一样。模板方法模式就是干这个的。

核心定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

二、模式结构:三个角色

这个模式的结构非常清晰,我画个图给你看:

AbstractClass(抽象类) + templateMethod() + primitiveOperation1() + primitiveOperation2() + hook() ← 钩子方法 ConcreteClass(具体子类) 模板方法定义算法骨架 基本方法由子类实现 钩子方法提供扩展点

你看,这个结构其实就三层:

  • 抽象类(AbstractClass):定义模板方法和基本方法。模板方法里规定了算法步骤,基本方法留给子类去实现。
  • 具体子类(ConcreteClass):实现父类定义的基本方法。可以有多个子类,每个子类实现不同的细节。
  • 钩子方法(Hook):这是个小彩蛋。父类定义一个默认实现,子类可以选择性地重写它,用来控制模板方法的某些步骤是否执行。

三、代码实战:咖啡与茶的制作

来,我写个经典例子。你一看就明白。

// 抽象类:饮料制作器
public abstract class BeverageMaker {
    
    // 模板方法 —— 算法骨架,用final防止子类修改
    public final void makeBeverage() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {  // 钩子方法
            addCondiments();
        }
    }
    
    // 基本方法:烧水(所有饮料都一样)
    private void boilWater() {
        System.out.println("烧开水...");
    }
    
    // 基本方法:冲泡(子类实现)
    protected abstract void brew();
    
    // 基本方法:倒杯(所有饮料都一样)
    private void pourInCup() {
        System.out.println("倒入杯中...");
    }
    
    // 钩子方法:默认返回true,子类可重写
    protected boolean customerWantsCondiments() {
        return true;
    }
    
    // 基本方法:加料(子类实现)
    protected abstract void addCondiments();
}

// 具体子类:咖啡
public class CoffeeMaker extends BeverageMaker {
    
    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡粉...");
    }
    
    @Override
    protected void addCondiments() {
        System.out.println("加糖和牛奶...");
    }
    
    @Override
    protected boolean customerWantsCondiments() {
        // 这里可以加逻辑,比如询问用户
        return true;
    }
}

// 具体子类:茶
public class TeaMaker extends BeverageMaker {
    
    @Override
    protected void brew() {
        System.out.println("用热水浸泡茶叶...");
    }
    
    @Override
    protected void addCondiments() {
        System.out.println("加柠檬...");
    }
    
    @Override
    protected boolean customerWantsCondiments() {
        // 钩子方法:用户可以选择不加料
        return false;  // 纯茶,不加料
    }
}
我的经验:模板方法里的基本方法,我习惯把「所有子类都一样」的步骤写成具体方法(比如boilWater),把「需要子类自定义」的步骤写成抽象方法。这样代码复用率最高。

四、钩子方法:算法的开关

钩子方法这个设计,我觉得是模板方法模式最妙的地方。它让子类可以「干预」算法的执行流程,但又不用修改算法结构。

为什么会需要钩子?你想想看,有时候子类想跳过某一步,或者在某些条件下才执行某一步。如果没有钩子,子类就得重写整个模板方法,那模式就失去意义了。

我曾经在一个支付系统中用过这个模式。支付流程是固定的:校验参数 → 调用第三方 → 处理回调 → 更新订单。但有个特殊渠道,它不需要校验某些参数。我就在抽象类里加了个钩子方法 needValidate(),默认返回true,那个特殊渠道重写为false。完美解决。

注意:钩子方法不要滥用。我见过有人把模板方法里每个步骤都加上钩子,结果代码变得极其复杂。记住,钩子是用来处理「例外情况」的,不是用来「让所有步骤都可配置」的。

五、模板方法 vs 策略模式

很多初学者会把这两个模式搞混。我简单对比一下:

对比维度 模板方法模式 策略模式
核心思想 定义算法骨架,子类实现细节 定义算法族,可互相替换
复用方式 继承复用(父类控制流程) 组合复用(客户端选择策略)
控制权 父类控制算法流程 客户端控制策略选择
适用场景 多个子类有相同的算法步骤 需要动态切换算法
代码变化点 子类重写基本方法 新增策略实现类

说白了,模板方法模式是「我定好流程,你来填空」。策略模式是「我给你接口,你爱怎么实现都行,我随时换」。两个模式各有各的用武之地。

六、实际应用场景

我整理了几个我在项目中用过的场景:

  • JUnit测试框架:setUp()、tearDown() 就是典型的钩子方法。测试流程是固定的,但每个测试类可以自定义准备和清理工作。
  • Spring中的JdbcTemplate:execute()方法定义了数据库操作的骨架,具体的StatementCallback由调用者实现。
  • Servlet中的HttpServlet:doGet()、doPost() 都是钩子方法,service() 方法根据请求类型调用对应的处理方法。
  • 工作流引擎:审批流程的骨架固定,但每个节点的具体处理逻辑由子类实现。
避坑指南:我曾经在一个项目中,把模板方法设计得过于「灵活」,每个步骤都加了钩子。结果子类代码变得支离破碎,维护起来特别痛苦。后来我重构了,只保留了真正需要变动的钩子。记住:少即是多

七、优缺点总结

任何模式都有两面性,我客观说说:

优点

  • 代码复用性高:公共步骤写在父类,子类只需关注差异部分
  • 符合开闭原则:新增子类不影响现有代码
  • 控制反转:父类控制算法流程,子类只负责实现细节

缺点

  • 继承的固有缺陷:子类与父类耦合度高
  • 类数量增加:每个不同的实现都需要一个子类
  • 理解成本:初学者可能搞不清模板方法和钩子方法的关系

嗯,模板方法模式就讲到这里。这个模式看着简单,但用好了真的能写出很优雅的代码。你写框架或者公共组件的时候,不妨想想——能不能把不变的流程固定下来,把变动的部分留给使用者去实现?这就是模板方法模式的精髓。


公众号:蓝海资料掘金营,微信deep3321