杰瑞科技汇

模板方法模式如何解耦Java代码逻辑?

这是一个非常基础且常用的行为型设计模式,它的核心思想是 “定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。”

模板方法模式如何解耦Java代码逻辑?-图1
(图片来源网络,侵删)

核心思想

想象一下,我们想制作一杯饮料,比如咖啡和茶,它们的制作流程非常相似:

  1. 烧水
  2. 冲泡
  3. 倒入杯中
  4. 添加辅料

对于咖啡和茶,步骤 1、3、4 可能是完全一样的,但步骤 2 “冲泡” 的内容不同,咖啡是 “用沸水冲泡咖啡粉”,而茶是 “用浸泡茶叶”。

模板方法模式就是利用这种场景:

  • 模板方法:定义了整个制作流程的骨架,也就是那 4 个步骤的执行顺序。
  • 具体步骤:将那些可能不同的步骤(如冲泡)定义为抽象方法,交给子类(咖啡或茶)去实现。
  • 不变部分:那些固定不变的步骤(如烧水)在父类中已经实现。

这样,我们既复用了代码(公共流程),又实现了灵活性和扩展性(不同子类的特定实现)。

模板方法模式如何解耦Java代码逻辑?-图2
(图片来源网络,侵删)

模式结构

模板方法模式主要包含两个角色:

抽象类

  • 模板方法:定义了算法的骨架,通常是 final 方法,以防止子类覆盖它,它按顺序调用基本操作。
  • 基本操作:包括:
    • 抽象方法:由子类必须实现的方法,代表算法中的可变部分。
    • 钩子方法:由子类可以选择性覆盖的方法,用于在特定点“挂钩”子类的行为,为算法提供额外的灵活性。
    • 具体方法:由父类直接实现的方法,代表算法中的不变部分。

具体子类

  • 继承抽象类。
  • 实现抽象类中声明的抽象方法,以完成算法中特定步骤的实现。
  • 可以选择性地覆盖钩子方法,来影响模板方法的执行流程。

代码示例(经典饮品制作)

我们来用经典的“制作咖啡和茶”的例子来演示。

第一步:定义抽象类 Beverage (饮品)

这个类将定义制作饮品的模板方法。

// 抽象类,定义了制作饮品的模板
public abstract class Beverage {
    // 模板方法 - final,防止子类修改算法流程
    public final void prepareRecipe() {
        boilWater();       // 1. 烧水
        brew();            // 2. 冲泡 (由子类实现)
        pourInCup();       // 3. 倒入杯中
        addCondiments();   // 4. 添加辅料 (由子类实现)
    }
    // 基本操作 - 具体方法,算法不变的部分
    private void boilWater() {
        System.out.println("1. 烧开水...");
    }
    // 基本操作 - 具体方法,算法不变的部分
    private void pourInCup() {
        System.out.println("3. 将饮品倒入杯中...");
    }
    // 基本操作 - 抽象方法,算法可变的部分,由子类实现
    protected abstract void brew();
    // 基本操作 - 抽象方法,算法可变的部分,由子类实现
    protected abstract void addCondiments();
}

第二步:创建具体子类

Coffee (咖啡)

// 具体子类,制作咖啡
public class Coffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println("2. 用沸水冲泡咖啡粉...");
    }
    @Override
    protected void addCondiments() {
        System.out.println("4. 添加糖和牛奶...");
    }
}

Tea (茶)

// 具体子类,制作茶
public class Tea extends Beverage {
    @Override
    protected void brew() {
        System.out.println("2. 用热水浸泡茶叶...");
    }
    @Override
    protected void addCondiments() {
        System.out.println("4. 添加柠檬...");
    }
}

第三步:客户端测试

public class TemplateMethodPatternDemo {
    public static void main(String[] args) {
        System.out.println("--- 制作咖啡 ---");
        Beverage coffee = new Coffee();
        coffee.prepareRecipe();
        System.out.println("\n--- 制作茶 ---");
        Beverage tea = new Tea();
        tea.prepareRecipe();
    }
}

输出结果

--- 制作咖啡 ---
1. 烧开水...
2. 用沸水冲泡咖啡粉...
3. 将饮品倒入杯中...
4. 添加糖和牛奶...
--- 制作茶 ---
1. 烧开水...
2. 用热水浸泡茶叶...
3. 将饮品倒入杯中...
4. 添加柠檬...

从输出中可以清晰地看到,prepareRecipe() 这个模板方法控制了整个流程,而 brew()addCondiments() 的具体实现则由子类 CoffeeTea 决定。

模板方法模式如何解耦Java代码逻辑?-图3
(图片来源网络,侵删)

钩子方法

钩子方法是一种特殊的 protected 方法,它通常返回一个布尔值,用于控制模板方法中的某个步骤是否执行。

让我们给 Beverage 类添加一个钩子方法 customerWantsCondiments()

修改后的抽象类 Beverage

public abstract class BeverageWithHook {
    // 模板方法
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        // 使用钩子方法来决定是否执行添加辅料的步骤
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }
    // ... (boilWater, pourInCup, brew, addCondiments) 方法保持不变 ...
    // 钩子方法:默认为 true,子类可以覆盖
    protected boolean customerWantsCondiments() {
        return true; // 默认客户都想要辅料
    }
}

创建一个不需要辅料的饮品子类 TeaWithoutHook

public class TeaWithoutHook extends BeverageWithHook {
    @Override
    protected void brew() {
        System.out.println("2. 用热水浸泡茶叶...");
    }
    @Override
    protected void addCondiments() {
        System.out.println("4. 添加柠檬...");
    }
    // 覆盖钩子方法,表示客户不想要辅料
    @Override
    protected boolean customerWantsCondiments() {
        return false;
    }
}

客户端测试

public class TemplateMethodPatternDemo {
    public static void main(String[] args) {
        System.out.println("--- 制作咖啡 ---");
        BeverageWithHook coffee = new CoffeeWithHook(); // 假设 CoffeeWithHook 也实现了钩子
        coffee.prepareRecipe();
        System.out.println("\n--- 制作不加柠檬的茶 ---");
        BeverageWithHook tea = new TeaWithoutHook();
        tea.prepareRecipe();
    }
}

输出结果

--- 制作咖啡 ---
1. 烧开水...
2. 用沸水冲泡咖啡粉...
3. 将饮品倒入杯中...
4. 添加糖和牛奶...
--- 制作不加柠檬的茶 ---
1. 烧开水...
2. 用热水浸泡茶叶...
3. 将饮品倒入杯中...
// 注意:没有执行 "4. 添加柠檬..."

通过钩子方法,我们可以在不改变模板方法 prepareRecipe() 的情况下,灵活地控制某个步骤的执行。


优点和缺点

优点

  1. 代码复用:将公共的算法流程放在父类中,避免了子类中重复代码。
  2. 扩展性好:子类可以通过实现或覆盖方法来灵活地扩展算法,而无需改变算法的结构。
  3. 封装不变部分:将算法的骨架固定,将变化的部分交给子类,符合“开闭原则”(对扩展开放,对修改关闭)。
  4. 实现反向控制:通过父类调用子类的方法,实现了“好莱坞原则”:“别调用我们,我们会调用你”,子类只需要关注自己的实现,而不需要关心整个流程。

缺点

  1. 设计较为复杂:对于一些简单的逻辑,使用模板方法模式可能会显得小题大做,增加了系统的抽象性和理解难度。
  2. 父类对子类的依赖:父类需要声明一些抽象方法或钩子方法,这本身就意味着它依赖于子类的实现,如果子类没有正确实现,可能会导致运行时错误。

应用场景

模板方法模式在 Java 源码和日常开发中非常常见。

  1. Java Servlet 中的 HttpServlet

    • service() 方法是模板方法,它定义了处理 HTTP 请求的流程。
    • doGet(), doPost(), doPut() 等是抽象方法,由开发者继承 HttpServlet 后根据业务需求进行具体实现。
    • getLastModified() 是一个钩子方法,用于返回资源的最后修改时间。
  2. Java I/O 中的 InputStreamOutputStream

    • read() 方法(对于 BufferedInputStream 等包装类)是一个模板方法,它内部会调用 read() 的一个重载版本,这个重载版本是一个抽象方法,由具体的子类(如 FileInputStream)实现。
  3. Junit 中的 TestCase

    • runBare() 方法是模板方法,它定义了测试用例的执行流程(设置环境 -> 运行测试 -> 清理环境)。
    • setUp()tearDown() 是钩子方法,供子类覆盖以进行测试前后的准备工作。
  4. 业务流程处理:任何具有“固定流程,但步骤细节不同”的场景,

    • 数据库连接的创建、使用、关闭流程。
    • 文件处理的读取、处理、保存流程。
    • 报表生成的数据收集、格式化、导出流程。

模板方法模式是一种通过 “继承” 来实现代码复用和扩展的强大工具,它将算法的 “骨架”“细节” 分离,让开发者能够专注于实现业务逻辑的细节,而无需重复编写通用的流程代码,掌握这个模式对于编写结构清晰、易于维护和扩展的代码至关重要。

分享:
扫描分享到社交APP
上一篇
下一篇