设计模式-模板方法模式

发布于:2025-07-10 ⋅ 阅读:(16) ⋅ 点赞:(0)

写在前面

Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!


需求背景

以饮品店铺售卖的饮品制作流程为例,进行模板方法模式的学习,饮品种类较多,且部分制作流程都比较具有相似性,而在具体的部分步骤又不一样,可以清晰的理解出 "变" 与 "不变"。

每种饮品的制作流程大致都包含以下步骤:

  1. 准备原料

  2. 煮沸水或牛奶

  3. 冲泡主要成分(咖啡粉、茶叶等)

  4. 添加配料(糖、奶泡等)

  5. 装杯。

接下来,首先使用传统编码方式实现饮品的制作流程系统,然后分析其中的问题,最后应用模板方法模式进行重构,清晰的理解如何使用模板方法模式。

传统编码实现

为每种饮品都创建一个独立的类,每个类都包含完完整的制作流程。

AmericanCoffee
public class AmericanCoffee {

    public void prepare() {

        this.boilWater();

        this.brewCoffeeGrounds();

        this.pourInCup();

        this.addSugarAndMilk();

        System.out.println("美式咖啡制作完成!\n");

    }

    private void boilWater() {
        System.out.println("将水煮沸至90-95摄氏度");
    }

    private void brewCoffeeGrounds() {
        System.out.println("用热水冲泡咖啡粉");
    }

    private void pourInCup() {
        System.out.println("将咖啡倒入杯中");
    }

    private void addSugarAndMilk() {
        System.out.println("根据顾客要求添加糖和牛奶");
    }
}
  • 该类代表美式咖啡的制作流程,perpate() 方法是主要的公共接口,其中按照顺序调用其他私有方法来完成制作流程

  • 私有方法 boilWater()brewCoffeeGrounds()pourInCup()addSugarAndMilk()分别代表制作的每个步骤。

LatteCoffee
public class LatteCoffee {

    public void prepare() {

        this.boilWater();

        this.brewCoffeeGrounds();

        this.addSteamedMilk();

        this.addFoam();

        this.pourInCup();

        System.out.println("拿铁咖啡制作完成!\n");

    }

    private void boilWater() {
        System.out.println("将水煮沸至90-95摄氏度");
    }

    private void brewCoffeeGrounds() {
        System.out.println("用热水冲泡咖啡粉制作浓缩咖啡");
    }

    private void addSteamedMilk() {
        System.out.println("加入煮熟的牛奶");
    }

    private void addFoam() {
        System.out.println("在顶部加入奶泡");
    }

    private void pourInCup() {
        System.out.println("将混合物导入杯中");
    }
    
}
  • 该类代表拿铁咖啡的制作流程,与美式咖啡相比,步骤有所不同,增加了 addSteamedMilk()addFoam()

GreenTea
public class GreenTea {

    public void prepare() {

        this.boilWater();

        this.steepTeaBag();

        this.pourInCup();

        this.addLemon();

        System.out.println("绿茶制作完成!\n");

    }

    private void boilWater() {
        System.out.println("将水煮沸至80摄氏度");
    }

    private void steepTeaBag() {
        System.out.println("浸泡绿茶茶包3-5分钟");
    }

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

    private void addLemon() {
        System.out.println("根据顾客要求添加柠檬");
    }

}
  • 该类代表绿茶的制作流程与咖啡相关类相比,绿茶的制作步骤有明显不同,例如使用steepTeaBag()而不是brewCoffeeGrounds(),即使是相似的步骤如 boilWater() 其具体实现页不同,及其添加的配料也不同。

测试类
public class TemplateTest {

    @Test
    public void test_beverage() {

        // 制作美式咖啡
        AmericanCoffee americanCoffee = new AmericanCoffee();
        System.out.println("=== 制作美式咖啡 ===");
        americanCoffee.prepare();

        // 制作拿铁咖啡
        LatteCoffee latteCoffee = new LatteCoffee();
        System.out.println("=== 制作拿铁咖啡 ===");
        latteCoffee.prepare();

        // 制作绿茶
        GreenTea greenTea = new GreenTea();
        System.out.println("=== 制作绿茶 ===");
        greenTea.prepare();
    }
}
运行结果
=== 制作美式咖啡 ===
将水煮沸至90-95摄氏度
用热水冲泡咖啡粉
将咖啡倒入杯中
根据顾客要求添加糖和牛奶
美式咖啡制作完成!

=== 制作拿铁咖啡 ===
将水煮沸至90-95摄氏度
用热水冲泡咖啡粉制作浓缩咖啡
加入煮熟的牛奶
在顶部加入奶泡
将混合物导入杯中
拿铁咖啡制作完成!

=== 制作绿茶 ===
将水煮沸至80摄氏度
浸泡绿茶茶包3-5分钟
将茶倒入杯中
根据顾客要求添加柠檬
绿茶制作完成!


Process finished with exit code 0

传统编码方式的问题分析

1.代码重复

不难发现三个饮品类中有许多相似甚至完全相同的代码:

  • 所有类都有一个 prepare() 方法,用于协调整个制作流程。

  • 每个类都遵循相似的制作步骤顺序:准备原料、加热、冲泡/浸泡、倒入杯中、添加辅料

2.维护困难

当需要修改共同的步骤时,必须修改所有相关类,例如,如果需要在饮品的制作流程中添加 质量检查 步骤,则需要修改三个类的 prepare() 方法:

增加 qualityCheck() 方法
private void qualityCheck() {
    System.out.println("进行质量检查");
}
修改 prepare() 方法
public void prepare() {

    this.boilWater();

    this.brewCoffeeGrounds();

    this.pourInCup();

    this.addSugarAndMilk();

    this.qualityCheck();

    System.out.println("美式咖啡制作完成!\n");

}
  • 当需要在所有饮品中添加一个新步骤时,必须修改每个类的 perpate() 方法。

3.扩展性差

当需要添加新的饮品种类时,必须创建一个全新的类,并重新实现所有步骤

模板方法模式

定义

模板方法模式定义了一个算法的骨架,将一些步骤的实现延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 模板方法模式就像是一个食谱:它规定了制作一道菜的基本步骤和顺序,但允许厨师根据自己的喜好调整某些步骤的具体做法(如调料的用量,火候的控制等)。

结构

  1. 抽象类:定义了一个模板方法,该方法包含算法的骨架,声明算法各步骤的抽象方法,由子类实现,可以包含一些具体方法和钩子方法。

  2. 具体类:实现抽象类中的抽象方法,为算法的特定步骤提供具体实现。

关键特性

  1. 算法骨架:模板方法定义了算法的基本结构和步骤顺序,这部分通常被声明为 final,防止子类修改。

  2. 抽象步骤:算法中的某些步骤被声明为抽象方法,必须由子类实现,这些步骤通常是算法中变化的步骤。

  3. 具体步骤:算法中的某些步骤在抽象类中已有具体实现,所有子类共享这些实现。

  4. 钩子方法:这些是在抽象类中已有默认实现的方法,子类可以选择性的重写它们,钩子方法为子类提供了额外的扩展点,使得子类可以在算法的特定点插入自定义行为。

重构饮品制作系统

重构思路

  1. 创建一个抽象的 Beverage 类,定义 prepareBeverage() 模板方法,该方法包含制作饮品的通用步骤顺序。

  2. Beverage 类中声明一些抽象方法(如 brew()addCondiments()),这些方法代表不同饮品之间变化的步骤。

  3. Beverage 类中实现一些具体方法(如 boilWater()pourInCup()),这些方法代表所有饮品共享的步骤。

  4. 创建具体的饮品类(如 CoffeeTea),继承 Beverage 类并实现抽象方法。

  5. 可以添加钩子方法(如 customerWantsCondiments()),允许子类决定是否执行某些可选步骤。

重构实现

创建抽象饮品类
public abstract class Beverage {

    public final void prepareBeverage() {

        this.boiWater();

        this.brew();

        this.pourInCup();

        if (this.customerWantsCondiments()) {
            addCondiments();
        }

        System.out.println(this.getBeverageName() + "制作完成!\n");

    }

    /**
     * 煮水 所有饮品通用步骤
     */
    protected void boiWater() {
        System.out.println("将水煮沸");
    }

    /**
     * 冲泡 子类实现
     */
    protected abstract void brew();

    /**
     * 倒入杯中 所有饮品通用步骤
     */
    protected void pourInCup() {
        System.out.println("将饮品倒入杯中");
    }

    /**
     * 添加调料
     */
    protected abstract void addCondiments();

    /**
     * 获取饮品名称
     */
    protected abstract String getBeverageName();

    /**
     * 钩子方法,决定是否添加调料
     * 默认返回 true,子类可以重写
     */
    protected boolean customerWantsCondiments() {
        return true;
    }
}
  • Beverage是一个抽象类,定义了饮品制作的基本流程

  • prepareBeverage() 是模板方法,声明为 final 防止子类重写,定义了制作饮品的算法骨架

  • boiWater()pourInCup() 是具体方法,提供了所有饮品共享的默认实现

  • brew()addCondiments()getBeverageName() 是抽象方法,必须由子类实现

  • customerWantsCondiments() 是钩子方法,提供了默认实现,子类可选择性的重写。

创建具体咖啡类
public abstract class Coffee extends Beverage {

    /**
     * 实现 brew 方法,定义咖啡的冲泡过程
     */
    @Override
    protected void brew() {
        System.out.println("用热水冲泡咖啡粉");
    }

}
  • Coffee 继承自 Beverage,是所有咖啡类型的父类,实现了 brew() 方法,定义了咖啡的通用冲泡过程,但没有实现 Beverage 中的所有抽象方法。

创建具体茶类
public abstract class Tea extends Beverage {

    /**
     * 实现brew方法,定义茶的冲泡过程
     */
    @Override
    protected void brew() {
        System.out.println("浸泡茶包");
    }

}
  • Tea 继承自 Beverage,是所有茶类型的父类,同样只实现了 brew() 方法,定义了茶的通用冲泡过程,但并未实现所有抽象方法。

创建具体饮品类
美式咖啡类
public class AmericanCoffee extends Coffee {

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

    @Override
    protected String getBeverageName() {
        return "美式咖啡";
    }
}
  • AmericanCoffee 继承自 Coffee,是一个具体的咖啡类型,实现了 addCondiments() 方法,定义了美式咖啡特有的调料,实现了 getBeverageName() 方法 返回饮品名称。

拿铁咖啡类
public class LatteCoffee extends Coffee {

    @Override
    protected void addCondiments() {
        System.out.println("添加蒸煮的牛奶和奶泡");
    }

    @Override
    protected String getBeverageName() {
        return "拿铁咖啡";
    }

    @Override
    protected void boiWater() {
        System.out.println("将水煮沸至85-90摄氏度");
    }
}
  • LatteCoffee 继承自 Coffee,实现了 addCondiments()getBeverageName() 方法,并且重写了 boilWater() 方法,该实例展示了模板方法模式的灵活性。

绿茶类
public class GreenTea extends Tea {

    @Override
    protected void addCondiments() {
        System.out.println("添加柠檬");
    }

    @Override
    protected String getBeverageName() {
        return "绿茶";
    }

    @Override
    protected void boiWater() {
        System.out.println("将水煮沸至80摄氏度");
    }
}
  • GreenTea 继承自 Tea,是一个具体的茶类型,实现了必要抽象方法,并重写了 boilWater() 方法,适应绿茶的特殊需求。

无糖绿茶类
public class SugarLessGreenTea extends GreenTea {

    @Override
    protected String getBeverageName() {
        return "无糖绿茶";
    }

    @Override
    protected boolean customerWantsCondiments() {
        return false;
    }
}
  • SugarLessGreenTea 继承自 GreenTea,是一个绿茶的变种,重写了 customerWantsCondiments() 钩子方法,返回 false 表示不需要添加调料,重写了 getBeverageName() 方法,展示了钩子方法的用户,允许子类控制算法中某些步骤的执行。

测试类
public class TemplateTest {

    @Test
    public void test_beverage() {

        System.out.println("=== 制作美式咖啡 ===");
        Beverage americanCoffee = new AmericanCoffee();
        americanCoffee.prepareBeverage();

        System.out.println("=== 制作拿铁咖啡 ===");
        Beverage latteCoffee = new LatteCoffee();
        latteCoffee.prepareBeverage();

        System.out.println("=== 制作绿茶 ===");
        Beverage greenTea = new GreenTea();
        greenTea.prepareBeverage();

        System.out.println("=== 制作无糖绿茶 ===");
        Beverage sugarLessGreenTea = new SugarLessGreenTea();
        sugarLessGreenTea.prepareBeverage();

    }

}
运行结果
=== 制作美式咖啡 ===
将水煮沸
用热水冲泡咖啡粉
将饮品倒入杯中
添加糖和牛奶
美式咖啡制作完成!

=== 制作拿铁咖啡 ===
将水煮沸至85-90摄氏度
用热水冲泡咖啡粉
将饮品倒入杯中
添加蒸煮的牛奶和奶泡
拿铁咖啡制作完成!

=== 制作绿茶 ===
将水煮沸至80摄氏度
浸泡茶包
将饮品倒入杯中
添加柠檬
绿茶制作完成!

=== 制作无糖绿茶 ===
将水煮沸至80摄氏度
浸泡茶包
将饮品倒入杯中
无糖绿茶制作完成!


Process finished with exit code 0

重构后优势

  1. 重复代码减少 在传统实现中,相同或相似的方法(如 boilWater()pourInCup())在多个类中重复出现,重构后,这些共同的方法被提取到抽象类 Beverage 中,通过引入中间抽象类(如 CoffeeTea),进一步减少了代码重复。

  2. 提高可维护性 在传统实现中,修改共同步骤需要修改所有相关类,重构后,当需要修改算法结构或添加新步骤时,只需要修改抽象类中的模板方法,所有子类都会自动继承这些变化。

  3. 增强扩展性 在传统实现中,添加新饮品需要创建全新的类并复制大量代码,重构后,只需要创建一个新的子类并实现特定的抽象方法,添加新饮品变得非常简单,只需要实现几个特定方法,而不需要重新实现整个制作流程。

  4. 逻辑变得清晰 模板方法清晰地定义了饮品制作的整体流程和步骤顺序,使得代码更易于理解和维护,任何人查看时都能立即理解饮品制作的基本流程,而不需要查看多个类。

长话短说

核心思想

模板方法模式将算法分为两部分:

  • 不变的部分:算法的整体结构和步骤顺序,由抽象类中的模板方法定义。

  • 变化的部分:算法中特定步骤的具体实现,由子类通过重写抽象方法提供。 这种分离使得算法的结构保持稳定,同时允许不同的实现方式。

实施步骤

  1. 分析算法,识别结构和变化点 首先,分析目标算法,识别其中的固定结构和可能变化的部分(哪些步骤对所有实现都是相同的?哪些步骤在不同实现中有所不同?哪些步骤是可选的,可能在某些实现中被跳过?) 例如:在饮品制作流程中,固定步骤:煮水、倒入杯中,变化步骤:冲泡方式、添加的调料,可选步骤:添加调料(某些饮品可能不需要)。

  2. 创建抽象类,定义模板方法 创建一个抽象类,在其中定义模板方法,该方法应该被声明为 final,防止子类重写,包含算法的完整步骤序列,调用抽象算法、具体方法和钩子方法。

public abstract class AbstractClass{
 // 模板方法,定义算法结构
 public final void templateMethod(){
  //算法步骤序列
  step1();
  step2();
  if(hook()){
   step3();
  }
  step4();
 
 }
 
 // 其他方法....
}
  1. 声明抽象方法和钩子方法 在抽象类中声明必要的抽象方法和钩子方法: 抽象方法:必须由子类实现的方法,代表算法中变化的部分。 钩子方法:有默认实现但可被子类重写的方法,通常用于控制算法的可选部分。

public abstract class AbstractClass{
 // 模板方法...
 
 // 抽象方法,必须由子类实现
 protected abstract void step2();
 
 // 具体方法,所有子类共享
 protected void step1(){
  // 默认实现
 }
 
 protected void step4(){
  // 默认实现
 }
 
 // 钩子方法,子类可选择性重写
 protected boolean hook(){
  return true;
 }
}
  1. 实现具体子类 创建具体子类,继承抽象类并实现所有抽象方法,根据需要,子类也可以重写钩子方法来控制算法的可选部分。

public class ConcreteClass extends AbstractClass{
 @Override
 protected void step2(){
  // 具体实现
 }
 
 @Override
 protected boolean hook(){
  //重写钩子方法
  return false;
 }
}
  1. 考虑添加中间抽象类 如果有多个相似的子类,考虑添加中间抽象类来进一步减少代码重复,中间抽象类可以实现部分抽象方法,为特定的子类提供共同实现。

在实例中,我们添加了 CoffeeTea 中间抽象类,它们实现了 brew() 方法,为各自类别的饮品提供了共同实现。

注意事项

在使用模板方法时,应注意以下问题:

  1. 保持模板方法简洁明了 模板方法应该清晰的表达算法的整体结构,避免过于复杂的逻辑,如果模板方法变得复杂,应考虑将其分解为多个更小的方法。

合理机构
public final void templateMethod(){
 step1();
 step2();
 if(hook()){
  step3();
 }
 step4();
}
避免如下结构
public final void templateMethod(){

 step1();
 if(condition1()){
  step2a();
  if(condition2()){
   step2b();
  } else {
   step2c();
  }
  
 } else {
  step2d();
 }
 // 更多复杂的逻辑... 
}
  1. 合理使用钩子方法 钩子方法是模板方法模式的强大特性,但应该合理使用,只为真正需要子类控制的点提供钩子方法,为钩子方法提供合理的默认实现,清晰的命名钩子方法,表明其用途。

合理方法
protected boolean shouldLogExecution(){
 return false;
}
避免模糊的命名
protected boolean hook1(){
 return true;
}
  1. 避免过度抽象 不是所有的方法都需要抽象,只将真正需要子类定制的步骤声明为抽象方法,将共同的实现放在具体方法中。

// 合理结构
protected void commonOperation(){
 //所有子类共享的实现
}

protected abstract void varyingOperation();

// 避免不必要的抽象
protected abstract void opertaion1();
protected abstract void opertaion2();
protected abstract void opertaion3();
protected abstract void opertaion4();
protected abstract void opertaion5();

  1. 考虑使用组合代替继承 虽然模板方法模式基于继承,但在某些情况下,使用组合可能更灵活,例如,可以将变化的步骤实现为策略对象,通过组合而非继承来定制算法。

public class TemplateWithStrategy(){
 private Strategy strategy;
 
 public TemplateWithStrategy(Strategy strategy){
  this.strategy = strategy;
 }
 
 public final void templateMethod(){
  step1();
  // 使用策略对象替代抽象方法
  strategy.execute(); 
  step3();
 }
}
  1. 遵循单一职责原则 确保抽象类和具体类都遵循单一职责原则,抽象类应该只关注算法的结构,而具体类应该只关注特定步骤的实现。


网站公告

今日签到

点亮在社区的每一天
去签到