【设计模式】(22)模板方法模式


模板方法模式(Template Method Pattern)教程


一、模式定义

模板方法模式在父类中定义了一个算法的骨架,允许子类在不改变算法结构的前提下重写某些特定步骤。
核心目标:复用公共流程,差异化实现细节,确保算法步骤的稳定性和扩展性。


二、适用场景

  1. 统一流程,差异细节:多个类有相同流程但某些步骤实现不同(如数据解析、文档生成)。
  2. 框架设计:框架定义核心流程,用户通过子类扩展具体行为(如Spring JdbcTemplate)。
  3. 避免重复代码:将重复的代码逻辑提取到父类模板中。
  4. 控制子类扩展:父类可声明某些步骤为 final 防止子类修改核心流程。

典型应用

  • Java Servlet 的 doGet()doPost() 方法。
  • 游戏引擎的 GameLoop(初始化 → 更新 → 渲染 → 清理)。
  • 测试框架的 TestCase 生命周期(setup → test → teardown)。

三、模式结构

角色说明
  1. 抽象类(Abstract Class)
    定义模板方法和算法骨架,包含:
    • 模板方法:声明为 final,定义不可变的算法步骤顺序。
    • 具体方法:已实现的通用步骤(如公共代码)。
    • 抽象方法:必须由子类实现的步骤。
    • 钩子方法(Hook):可选重写的空方法,提供扩展点。
  2. 具体子类(Concrete Class)
    实现抽象类中的抽象方法,覆盖钩子方法(可选)。

四、代码案例

案例1:饮料制作流程

咖啡和茶的制作流程相同(烧水 → 冲泡 → 倒杯 → 加调料),但冲泡和加调料步骤不同。

步骤1:定义抽象类(模板)
public abstract class Beverage {
    // 模板方法(final防止子类修改流程)
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) { // 钩子方法控制是否加调料
            addCondiments();
        }
    }

    // 具体方法(公共步骤)
    private void boilWater() {
        System.out.println("烧水");
    }

    private void pourInCup() {
        System.out.println("倒入杯子");
    }

    // 抽象方法(子类必须实现)
    protected abstract void brew();
    protected abstract void addCondiments();

    // 钩子方法(子类可选覆盖)
    protected boolean customerWantsCondiments() {
        return true; // 默认加调料
    }
}
步骤2:实现具体子类
// 咖啡
public class Coffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println("冲泡咖啡粉");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加糖和牛奶");
    }

    // 覆盖钩子方法:用户可以选择不加调料
    @Override
    protected boolean customerWantsCondiments() {
        String answer = getUserInput();
        return answer.toLowerCase().startsWith("y");
    }

    private String getUserInput() {
        System.out.print("是否加糖和牛奶?(y/n)");
        // 模拟用户输入(实际中可读取控制台输入)
        return "y";
    }
}

// 茶
public class Tea extends Beverage {
    @Override
    protected void brew() {
        System.out.println("浸泡茶叶");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加柠檬");
    }
}
步骤3:客户端调用
public class Client {
    public static void main(String[] args) {
        Beverage coffee = new Coffee();
        coffee.prepareRecipe();
        /* 输出:
           烧水
           冲泡咖啡粉
           倒入杯子
           是否加糖和牛奶?(y/n)
           加糖和牛奶
        */

        Beverage tea = new Tea();
        tea.prepareRecipe();
        /* 输出:
           烧水
           浸泡茶叶
           倒入杯子
           加柠檬
        */
    }
}

案例2:数据库操作模板

封装数据库操作的通用流程:连接 → 执行SQL → 关闭连接。

步骤1:抽象类定义模板
public abstract class DatabaseTemplate {
    public final void execute(String sql) {
        Connection conn = null;
        try {
            conn = getConnection();      // 抽象方法
            executeStatement(conn, sql); // 具体方法
        } catch (SQLException e) {
            handleError(e);              // 钩子方法
        } finally {
            closeConnection(conn);       // 具体方法
        }
    }

    // 抽象方法:由子类实现数据库连接
    protected abstract Connection getConnection() throws SQLException;

    // 具体方法:执行SQL(可被子类覆盖)
    protected void executeStatement(Connection conn, String sql) throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            stmt.execute(sql);
            System.out.println("SQL执行成功");
        }
    }

    // 钩子方法:默认错误处理(子类可覆盖)
    protected void handleError(SQLException e) {
        System.err.println("数据库错误:" + e.getMessage());
    }

    // 具体方法:关闭连接
    private void closeConnection(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                System.err.println("关闭连接失败:" + e.getMessage());
            }
        }
    }
}
步骤2:具体子类实现
public class MySQLDatabase extends DatabaseTemplate {
    @Override
    protected Connection getConnection() throws SQLException {
        String url = "jdbc:mysql://localhost:3306/mydb";
        return DriverManager.getConnection(url, "user", "password");
    }

    // 覆盖错误处理钩子
    @Override
    protected void handleError(SQLException e) {
        System.err.println("MySQL错误代码:" + e.getErrorCode());
    }
}

五、模板方法模式 vs 策略模式

对比项 模板方法模式 策略模式
实现方式 继承:子类覆盖父类方法 组合:通过接口注入策略对象
灵活性 结构固定,仅能扩展部分步骤 可完全替换算法
代码复用 父类集中通用逻辑 策略对象独立,无复用
适用场景 流程固定,步骤差异小 算法差异大,需动态替换

六、常见问题

  1. 如何防止子类修改模板方法?

    • 将模板方法声明为 final
  2. 钩子方法的典型应用场景?

    • 提供可选扩展点(如案例1中的调料选择)。
  3. 模板方法模式是否违反开闭原则?

    • 不违反。新增子类无需修改父类代码。
  4. 如何处理多个可变步骤?

    • 为每个可变步骤定义抽象方法或钩子方法。

七、习题练习

基础题

实现一个 文件导出模板,流程:打开文件 → 写入数据 → 关闭文件。要求:

  • TXT文件直接写入文本。
  • CSV文件在数据前添加表头。
提高题

设计一个 跨平台UI渲染模板,流程:加载资源 → 布局 → 绘制。不同平台(Windows、macOS)实现资源加载和绘制细节。

思考题

在微服务架构中,如何利用模板方法模式统一服务调用流程(如鉴权 → 参数校验 → 执行逻辑 → 记录日志)?


八、推荐拓展

  1. 框架级应用

    • Spring 的 JdbcTemplate:封装JDBC操作流程(获取连接、执行SQL、释放资源)。
    • JUnit 的 TestCasesetUp()testXxx()tearDown()
  2. 设计模式组合

    • 结合模板方法模式与工厂方法模式,在父类中定义对象创建流程。
    • 结合模板方法模式与观察者模式,在算法步骤中触发事件通知。
  3. 设计原则深化

    • 好莱坞原则:“别找我们,我们找你”——父类控制流程,子类仅提供实现。
    • 单一职责原则:每个子类仅关注自身步骤的实现。
  4. 实际项目应用

    • 工作流引擎中的任务处理模板。
    • 电商订单处理流程(校验 → 扣库存 → 生成物流单 → 通知用户)。

通过本教程,你可以掌握模板方法模式在算法复用和流程控制中的核心技巧,并能够灵活应用于需要统一框架但支持扩展的场景。若有疑问或需要代码调试,欢迎随时交流!

你可能感兴趣的:(设计模式,Java教程,设计模式,模板方法模式)