1.概述
模板方法模式是一种行为设计模式, 它定义了一个算法的框架,并将具体步骤延迟到子类中实现。在该模式中,父类中定义一个模板方法来描述算法的基本流程,在这个过程中,某些步骤可以通过抽象方法或空置来延迟到子类中实现。
通常情况下,当我们处理一些相似的任务时,会发现这些任务之间有很多共性,只是其中一些步骤不同而已。如果每次都重复编写代码,既费时又容易出错。因此,为了解决这个问题,就产生了模板方法模式,它把这些共性代码抽象到父类中,子类只需要覆盖特定的步骤即可。
2.结构
模板方法模式包含以下几个角色:
抽象类(Abstract Class):定义抽象方法和模板方法,抽象方法由子类实现,模板方法描述算法的基本流程,并使用抽象方法来延迟到子类中实现:
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法;
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现;
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承;
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。
3.实例
3.1实例类比
如上模板方法可用于建造大量房屋。 标准房屋建造方案中可提供几个扩展点, 允许潜在房屋业主调整成品房屋的部分细节。
每个建造步骤 (例如打地基、 建造框架、 建造墙壁和安装水电管线等) 都能进行微调, 这使得成品房屋会略有不同。
3.2 案例
可以将模板方法模式类比于制作蛋糕的过程。在制作蛋糕时,有很多共性的步骤,例如准备材料、打好鸡蛋液、混合面粉和糖等,但是每种蛋糕可能会有一些特殊的步骤或要求。如果我们针对每种蛋糕都写一份完整的制作流程,那么代码量会非常大,而且维护起来也不方便。
因此,我们可以使用模板方法模式来解决这个问题。我们可以定义一个抽象的蛋糕制作类,其中包含了一系列基本的制作步骤,例如准备材料、打好鸡蛋液、混合面粉和糖等。然后,我们再定义具体的蛋糕制作类,例如水果蛋糕制作类、巧克力蛋糕制作类等,它们继承自抽象的蛋糕制作类,并实现其中的抽象方法,例如加入水果、加入巧克力等。
当需要制作某种特定的蛋糕时,我们只需要选择相应的具体类即可,无需重复编写制作流程,代码更加简洁易懂,也更易于维护。
3.3 结构分析
在上述案例中,模板方法模式的角色分别对应如下:
抽象类 Cake:是一个抽象的蛋糕制作类,其中包含了一系列基本的制作步骤,并定义了一个模板方法 make() 来封装这些基本步骤:
make() 方法:是一个具体方法,它包含了一系列基本的制作步骤,并且定义了整个算法的流程和骨架。它通过调用抽象方法和钩子方法来完成算法中的特殊步骤和可选步骤;
addSpecialIngredient() 方法:是一个抽象方法,用于在蛋糕制作中添加特殊食材。由于不同的蛋糕需要添加不同的特殊食材,因此将其定义为抽象方法,由子类实现;
needDecorate() 方法:是一个钩子方法,用于判断是否需要添加装饰。由于不同的蛋糕可能需要或者不需要装饰,因此将其定义为钩子方法,由子类决定是否进行装饰;
prepareIngredients()、mixEggLiquid()、mixFlourAndSugar()、bake() 和 addDecorate() 方法:都是具体方法,用于实现蛋糕制作中的基本步骤。这些方法在模板方法中被调用,但是它们的具体实现可以被子类所重写;
具体类 FruitCake 和 ChocolateCake:是 Cake 的具体子类,它们各自实现了抽象方法 addSpecialIngredient() 和 needDecorate() 来完成特殊的步骤,并对需要覆盖的基本步骤进行了重写。
类图如下:
3.4 具体实现
#include <iostream>
#include <string>
using namespace std;
//抽象类
class Cake {
public:
/**
* 制作蛋糕的模板方法
*/
void make() {
// 准备材料
prepareIngredients();
// 打好鸡蛋液
mixEggLiquid();
// 混合面粉和糖
mixFlourAndSugar();
// 加入特殊食材
addSpecialIngredient();
// 在烤箱中烘烤
bake();
if (needDecorate()) {
// 如果需要覆盖,再添加霜糖
addDecorate();
}
}
protected:
/**
* 抽象方法:加入特殊食材
*/
virtual void addSpecialIngredient()= 0;
/**
* 抽象方法:是否需要覆盖
*/
virtual bool needDecorate() = 0;
private:
/**
* 准备材料
*/
void prepareIngredients() {
std::cout << "准备材料..." << endl;
}
/**
* 打好鸡蛋液
*/
void mixEggLiquid() {
std::cout << "打好鸡蛋液..." << endl;
}
/**
* 混合面粉和糖
*/
void mixFlourAndSugar() {
std::cout << "混合面粉和糖..." << endl;
}
/**
* 在烤箱中烘烤
*/
void bake() {
std::cout << "在烤箱中烘烤..." << endl;
}
/**
* 添加装饰
*/
void addDecorate() {
std::cout << "给蛋糕上添加装饰..." << endl;
}
};
//具体类1
class FruitCake:public Cake {
/**
* 加入特殊食材:水果
*/
protected:
void addSpecialIngredient() {
std::cout << "将水果加入蛋糕中..." << endl;
}
/**
* 是否需要覆盖:不需要
*/
bool needDecorate() {
return false;
}
};
//具体类2
class ChocolateCake :public Cake {
protected:
/**
* 加入特殊食材:巧克力
*/
void addSpecialIngredient() {
std::cout << "将巧克力加入蛋糕中..." << endl;
}
/**
* 是否需要覆盖:需要
*/
bool needDecorate() {
return true;
}
};
int main() {
std::cout << "制作巧克力蛋糕" << endl;
Cake *chocolateCake = new ChocolateCake();
chocolateCake->make();
std::cout << "---------------------- " << endl;
std::cout << "制作水果蛋糕" << endl;
Cake *fruitCake = new FruitCake();
fruitCake->make();
return 0;
}
3.5 运行结果
4.模板方法优缺点
优点:
- 提高代码复用性:将相同的部分抽象出来成为模板,避免重复的代码,同时提高了代码的复用性。
- 便于维护:模板方法模式提供了稳定的结构,使得子类的实现变得简单明了,也更容易进行维护。
- 实现了反向控制:通过把不变行为的控制权交给父类,将变化行为留给子类来实现,实现了反向控制,符合“开闭原则”。
缺点:
- 模板方法中的步骤越多, 其维护工作就可能会越困难
5.应用场景
- 父类和子类之间共享一组行为或算法,并且其实现步骤基本相同,只有个别步骤的实现有所不同。
- 需要在多个项目或多个版本中重复使用相同的代码逻辑。
- 需要在代码运行时动态地指定某些部分的行为,而不是静态地实现在代码中。
- 多个程序或系统中存在类似的业务流程,但其中个别步骤的实现有所不同,可以使用模板方法模式将这些不同的实现抽象出来,由具体的子类去实现。