写在前面
Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!
需求背景
以饮品店铺售卖的饮品制作流程为例,进行模板方法模式的学习,饮品种类较多,且部分制作流程都比较具有相似性,而在具体的部分步骤又不一样,可以清晰的理解出 "变" 与 "不变"。
每种饮品的制作流程大致都包含以下步骤:
- 准备原料
- 煮沸水或牛奶
- 冲泡主要成分(咖啡粉、茶叶等)
- 添加配料(糖、奶泡等)
- 装杯。
接下来,首先使用传统编码方式实现饮品的制作流程系统,然后分析其中的问题,最后应用模板方法模式进行重构,清晰的理解如何使用模板方法模式。
传统编码实现
为每种饮品都创建一个独立的类,每个类都包含完完整的制作流程。
AmericanCoffee
csharp
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
csharp
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
csharp
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()
其具体实现页不同,及其添加的配料也不同。
测试类
csharp
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();
}
}
运行结果
diff
=== 制作美式咖啡 ===
将水煮沸至90-95摄氏度
用热水冲泡咖啡粉
将咖啡倒入杯中
根据顾客要求添加糖和牛奶
美式咖啡制作完成!
=== 制作拿铁咖啡 ===
将水煮沸至90-95摄氏度
用热水冲泡咖啡粉制作浓缩咖啡
加入煮熟的牛奶
在顶部加入奶泡
将混合物导入杯中
拿铁咖啡制作完成!
=== 制作绿茶 ===
将水煮沸至80摄氏度
浸泡绿茶茶包3-5分钟
将茶倒入杯中
根据顾客要求添加柠檬
绿茶制作完成!
Process finished with exit code 0
传统编码方式的问题分析
1.代码重复
不难发现三个饮品类中有许多相似甚至完全相同的代码:
- 所有类都有一个
prepare()
方法,用于协调整个制作流程。 - 每个类都遵循相似的制作步骤顺序:准备原料、加热、冲泡/浸泡、倒入杯中、添加辅料
2.维护困难
当需要修改共同的步骤时,必须修改所有相关类,例如,如果需要在饮品的制作流程中添加 质量检查 步骤,则需要修改三个类的 prepare()
方法:
增加 qualityCheck()
方法
csharp
private void qualityCheck() {
System.out.println("进行质量检查");
}
修改 prepare()
方法
kotlin
public void prepare() {
this.boilWater();
this.brewCoffeeGrounds();
this.pourInCup();
this.addSugarAndMilk();
this.qualityCheck();
System.out.println("美式咖啡制作完成!\n");
}
- 当需要在所有饮品中添加一个新步骤时,必须修改每个类的
perpate()
方法。
3.扩展性差
当需要添加新的饮品种类时,必须创建一个全新的类,并重新实现所有步骤
模板方法模式
定义
模板方法模式定义了一个算法的骨架,将一些步骤的实现延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 模板方法模式就像是一个食谱:它规定了制作一道菜的基本步骤和顺序,但允许厨师根据自己的喜好调整某些步骤的具体做法(如调料的用量,火候的控制等)。
结构
- 抽象类:定义了一个模板方法,该方法包含算法的骨架,声明算法各步骤的抽象方法,由子类实现,可以包含一些具体方法和钩子方法。
- 具体类:实现抽象类中的抽象方法,为算法的特定步骤提供具体实现。
关键特性
- 算法骨架:模板方法定义了算法的基本结构和步骤顺序,这部分通常被声明为
final
,防止子类修改。 - 抽象步骤:算法中的某些步骤被声明为抽象方法,必须由子类实现,这些步骤通常是算法中变化的步骤。
- 具体步骤:算法中的某些步骤在抽象类中已有具体实现,所有子类共享这些实现。
- 钩子方法:这些是在抽象类中已有默认实现的方法,子类可以选择性的重写它们,钩子方法为子类提供了额外的扩展点,使得子类可以在算法的特定点插入自定义行为。
重构饮品制作系统
重构思路
- 创建一个抽象的
Beverage
类,定义prepareBeverage()
模板方法,该方法包含制作饮品的通用步骤顺序。 - 在
Beverage
类中声明一些抽象方法(如brew()
、addCondiments()
),这些方法代表不同饮品之间变化的步骤。 - 在
Beverage
类中实现一些具体方法(如boilWater()
、pourInCup()
),这些方法代表所有饮品共享的步骤。 - 创建具体的饮品类(如
Coffee
、Tea
),继承Beverage
类并实现抽象方法。 - 可以添加钩子方法(如
customerWantsCondiments()
),允许子类决定是否执行某些可选步骤。
重构实现
创建抽象饮品类
csharp
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()
是钩子方法,提供了默认实现,子类可选择性的重写。
创建具体咖啡类
csharp
public abstract class Coffee extends Beverage {
/**
* 实现 brew 方法,定义咖啡的冲泡过程
*/
@Override
protected void brew() {
System.out.println("用热水冲泡咖啡粉");
}
}
Coffee
继承自Beverage
,是所有咖啡类型的父类,实现了brew()
方法,定义了咖啡的通用冲泡过程,但没有实现Beverage
中的所有抽象方法。
创建具体茶类
csharp
public abstract class Tea extends Beverage {
/**
* 实现brew方法,定义茶的冲泡过程
*/
@Override
protected void brew() {
System.out.println("浸泡茶包");
}
}
Tea
继承自Beverage
,是所有茶类型的父类,同样只实现了brew()
方法,定义了茶的通用冲泡过程,但并未实现所有抽象方法。
创建具体饮品类
美式咖啡类
typescript
public class AmericanCoffee extends Coffee {
@Override
protected void addCondiments() {
System.out.println("添加糖和牛奶");
}
@Override
protected String getBeverageName() {
return "美式咖啡";
}
}
AmericanCoffee
继承自Coffee
,是一个具体的咖啡类型,实现了addCondiments()
方法,定义了美式咖啡特有的调料,实现了getBeverageName()
方法 返回饮品名称。
拿铁咖啡类
typescript
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()
方法,该实例展示了模板方法模式的灵活性。
绿茶类
typescript
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()
方法,适应绿茶的特殊需求。
无糖绿茶类
typescript
public class SugarLessGreenTea extends GreenTea {
@Override
protected String getBeverageName() {
return "无糖绿茶";
}
@Override
protected boolean customerWantsCondiments() {
return false;
}
}
SugarLessGreenTea
继承自GreenTea
,是一个绿茶的变种,重写了customerWantsCondiments()
钩子方法,返回false
表示不需要添加调料,重写了getBeverageName()
方法,展示了钩子方法的用户,允许子类控制算法中某些步骤的执行。
测试类
csharp
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();
}
}
运行结果
diff
=== 制作美式咖啡 ===
将水煮沸
用热水冲泡咖啡粉
将饮品倒入杯中
添加糖和牛奶
美式咖啡制作完成!
=== 制作拿铁咖啡 ===
将水煮沸至85-90摄氏度
用热水冲泡咖啡粉
将饮品倒入杯中
添加蒸煮的牛奶和奶泡
拿铁咖啡制作完成!
=== 制作绿茶 ===
将水煮沸至80摄氏度
浸泡茶包
将饮品倒入杯中
添加柠檬
绿茶制作完成!
=== 制作无糖绿茶 ===
将水煮沸至80摄氏度
浸泡茶包
将饮品倒入杯中
无糖绿茶制作完成!
Process finished with exit code 0
重构后优势
- 重复代码减少 在传统实现中,相同或相似的方法(如
boilWater()
、pourInCup()
)在多个类中重复出现,重构后,这些共同的方法被提取到抽象类Beverage
中,通过引入中间抽象类(如Coffee
和Tea
),进一步减少了代码重复。 - 提高可维护性 在传统实现中,修改共同步骤需要修改所有相关类,重构后,当需要修改算法结构或添加新步骤时,只需要修改抽象类中的模板方法,所有子类都会自动继承这些变化。
- 增强扩展性 在传统实现中,添加新饮品需要创建全新的类并复制大量代码,重构后,只需要创建一个新的子类并实现特定的抽象方法,添加新饮品变得非常简单,只需要实现几个特定方法,而不需要重新实现整个制作流程。
- 逻辑变得清晰 模板方法清晰地定义了饮品制作的整体流程和步骤顺序,使得代码更易于理解和维护,任何人查看时都能立即理解饮品制作的基本流程,而不需要查看多个类。
长话短说
核心思想
模板方法模式将算法分为两部分:
- 不变的部分:算法的整体结构和步骤顺序,由抽象类中的模板方法定义。
- 变化的部分:算法中特定步骤的具体实现,由子类通过重写抽象方法提供。 这种分离使得算法的结构保持稳定,同时允许不同的实现方式。
实施步骤
- 分析算法,识别结构和变化点 首先,分析目标算法,识别其中的固定结构和可能变化的部分(哪些步骤对所有实现都是相同的?哪些步骤在不同实现中有所不同?哪些步骤是可选的,可能在某些实现中被跳过?) 例如:在饮品制作流程中,固定步骤:煮水、倒入杯中,变化步骤:冲泡方式、添加的调料,可选步骤:添加调料(某些饮品可能不需要)。
- 创建抽象类,定义模板方法 创建一个抽象类,在其中定义模板方法,该方法应该被声明为
final
,防止子类重写,包含算法的完整步骤序列,调用抽象算法、具体方法和钩子方法。
scss
public abstract class AbstractClass{
// 模板方法,定义算法结构
public final void templateMethod(){
//算法步骤序列
step1();
step2();
if(hook()){
step3();
}
step4();
}
// 其他方法....
}
- 声明抽象方法和钩子方法 在抽象类中声明必要的抽象方法和钩子方法: 抽象方法:必须由子类实现的方法,代表算法中变化的部分。 钩子方法:有默认实现但可被子类重写的方法,通常用于控制算法的可选部分。
csharp
public abstract class AbstractClass{
// 模板方法...
// 抽象方法,必须由子类实现
protected abstract void step2();
// 具体方法,所有子类共享
protected void step1(){
// 默认实现
}
protected void step4(){
// 默认实现
}
// 钩子方法,子类可选择性重写
protected boolean hook(){
return true;
}
}
- 实现具体子类 创建具体子类,继承抽象类并实现所有抽象方法,根据需要,子类也可以重写钩子方法来控制算法的可选部分。
less
public class ConcreteClass extends AbstractClass{
@Override
protected void step2(){
// 具体实现
}
@Override
protected boolean hook(){
//重写钩子方法
return false;
}
}
- 考虑添加中间抽象类 如果有多个相似的子类,考虑添加中间抽象类来进一步减少代码重复,中间抽象类可以实现部分抽象方法,为特定的子类提供共同实现。
在实例中,我们添加了 Coffee
和 Tea
中间抽象类,它们实现了 brew()
方法,为各自类别的饮品提供了共同实现。
注意事项
在使用模板方法时,应注意以下问题:
- 保持模板方法简洁明了 模板方法应该清晰的表达算法的整体结构,避免过于复杂的逻辑,如果模板方法变得复杂,应考虑将其分解为多个更小的方法。
合理机构
scss
public final void templateMethod(){
step1();
step2();
if(hook()){
step3();
}
step4();
}
避免如下结构
scss
public final void templateMethod(){
step1();
if(condition1()){
step2a();
if(condition2()){
step2b();
} else {
step2c();
}
} else {
step2d();
}
// 更多复杂的逻辑...
}
- 合理使用钩子方法 钩子方法是模板方法模式的强大特性,但应该合理使用,只为真正需要子类控制的点提供钩子方法,为钩子方法提供合理的默认实现,清晰的命名钩子方法,表明其用途。
合理方法
typescript
protected boolean shouldLogExecution(){
return false;
}
避免模糊的命名
typescript
protected boolean hook1(){
return true;
}
- 避免过度抽象 不是所有的方法都需要抽象,只将真正需要子类定制的步骤声明为抽象方法,将共同的实现放在具体方法中。
csharp
// 合理结构
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();
- 考虑使用组合代替继承 虽然模板方法模式基于继承,但在某些情况下,使用组合可能更灵活,例如,可以将变化的步骤实现为策略对象,通过组合而非继承来定制算法。
csharp
public class TemplateWithStrategy(){
private Strategy strategy;
public TemplateWithStrategy(Strategy strategy){
this.strategy = strategy;
}
public final void templateMethod(){
step1();
// 使用策略对象替代抽象方法
strategy.execute();
step3();
}
}
- 遵循单一职责原则 确保抽象类和具体类都遵循单一职责原则,抽象类应该只关注算法的结构,而具体类应该只关注特定步骤的实现。