目录
1.概述
在模板方法模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
模板方法模式是一种代码复用的基本技术,定义了一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构可重定义该算法的某些特定步骤。在使用模板方法时,很重要的一点是模板方法应当指明哪些操作是可以被重写的,以及哪些是必须被重写的。
2.结构
模板方法模式的UML类图如下:
角色定义:
**AbstractClass是抽象类:**其实也就是一抽象模板,定义并实现了一个模版方法。这个模版方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
**ConcreteClass实现类:**实现父类所定义的一个或多个抽象方法。每一个AbstractClass都可以有任意多个ConcreteClass与之对应,而每一个ConcreteClass都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。ConcreteClassA和ConcreteClassB是两个不同的实现类。
3.实现
抽象类定义:
cpp
class AbstractClass
{
public:
virtual ~AbstractClass() {}
virtual void primitiveOperation1() = 0;
virtual void primitiveOperation2() = 0;
void templateMethod() {
primitiveOperation1();
primitiveOperation2();
}
};
实现类定义:
cpp
class ConcreteClassA : public AbstractClass {
public:
void primitiveOperation1() override {
cout << "ConcreteClassA primitiveOperation1..." << endl;
}
void primitiveOperation2() override {
cout << "ConcreteClassA primitiveOperation2..." << endl;
}
};
class ConcreteClassB : public AbstractClass {
public:
void primitiveOperation1() override {
cout << "ConcreteClassB primitiveOperation1..." << endl;
}
void primitiveOperation2() override {
cout << "ConcreteClassB primitiveOperation2..." << endl;
}
};
测试例子:
cpp
int main()
{
std::unique_ptr<AbstractClass> a(new ConcreteClassA());
a->templateMethod();
std::unique_ptr<AbstractClass> b(new ConcreteClassB());
b->templateMethod();
return 0;
}
输出:
cpp
ConcreteClassA primitiveOperation1...
ConcreteClassA primitiveOperation2...
ConcreteClassB primitiveOperation1...
ConcreteClassB primitiveOperation2...
4.案例分析
4.1.经典模板方法
以游戏模拟为例
大多数棋类游戏都非常相似:游戏开始(完成不同形式的初始设置),玩家轮流出招,到决出胜利者,然后宣布哪个玩家获胜。不管游戏是什么-国际象棋、跳棋等-我们都可以按如下方式定义算法:
cpp
class Game
{
public:
void run()
{
start();
while(!have_winner())
{
take_turn();
}
cout << "player " << get_winner() << " wins.\n";
}
protected:
virtual void start() = 0;
virtual bool have_winner() = 0;
virtual void take_turn() = 0;
virtual int get_winner() = 0;
};
可以看到,函数run(),即运行游戏的函数,只是简单地调用了一系列其他函数。这些都是虚函数,并且具有protected的访问属性,因此它们不会被自己实例化的对象意外调用:
公平地说、其中的某些方法,尤其是返回类型为void的方法,不必定义为纯虚函数,例如,如果某个游戏没有明确的start()流程,将start()方法定义为纯虚丽数就违反了接口隔离原则,因为这个游戏并不需要start()接口,却还是不得不提供它的实现。我们可以制定一个包含无操作函数的虚方法的策略,但用模板方法模式,情况就不那么明确了。
现在,除了这些成员以外,我们定义了一些与游戏相关的公共成员-玩家数量和当前玩家的索引:
cpp
class Game
{
public:
explicit Game(int number_of_players):number_of_players(number_of_players{}
protected:
int current_player{ 0 };
int number_of_players;
};// other members omitted
从现在开始,可以扩展Game类来实现国际象棋游戏:
cpp
class Chess : public Game
{
public:
explicit chess():Game{ 2 }{}
protected:
void start() override {}
bool have_winner() override { return turns == max turns; }
void take_turn() override
{
turns++;
current_player=(current_player + 1)% number_of_players;
}
int get_winner() override { return current_player;}
private:
int turns{ 0 };
int max_turns{ 10 };
};
国际象棋游戏包含两个玩家,所以将这一信息传人构造函数。随后,我们重写所有必要的函数,实现一个非常简单的模拟游戏的逻辑,即在完成10轮操作后结束游戏。以下是程序的输出:
cpp
Starting a game of chess with 2 players
Turn 0 taken by player 0
Turn 1 taken by player 1
...
Turn 8 taken by player 0
Turn 9 taken by player 1
Player 0 wins.
这几乎就是所有的程序逻辑了!
4.2.函数式模板方法
虽然经典的模板方法利用了继承,现代C++也允许函数式模板方法的变种存在。在这种情况下,策略模式和模板方法模式之间的界限非常模糊,因为在这两种模式中,本质上涉及的都是高阶函数。
函数式模板方法需要定义一个单独的函数run_game(),它以模板类型作为参数。一如既往,在定义高阶函数时,我们有两个选项:
1.将接受的函数强制转换为函数指针、std::function或类似的结构。
2.使用模板模糊地定义参数。这让我们可以将不同的结构作为参数传递,比如仿函数和lambda 表达式。
我们的函数式方法将定义一个包含游戏相关信息的结构:
cpp
struct GameState
{
int current_player, winning_player;
int number_of_players;
};
我们现在像以前一样定义模板方法,唯一的区别是它不是任何类的一部分,因此,与重写函数不同,所有这些成员函数都将以模板参数的形式提供:
cpp
template<typename FnStartAction,
typename FnTakeTurnAction,
typename FnHaveWinnerAction>
void run_game(GameState initial_state,
FnStartAction start_action,
FnTakeTurnAction take_turn_action,
FnHaveWinnerAction have_winner_action)
{
GameState state = initial_state;
start_action(state);
while (!have_winner_action(state))
{
take_turn_action(state);
}
cout <<"player " << state.winning_player << " wins,\n";
}
run_game()函数接受一个初始状态,以及一堆函数或者类似函数的对象。这些函数可以在任意地方定义,可以使用仿函数,不过使用lambda表达式定义更简单:
cpp
int turn(o),max_turns(10);
Gamestate state{0,-1,2};
auto start = [](GameState& s)
{
cout << "Starting a game of chess with "<< s.number_of_players << " players\n";
};
auto take_turn =[&](GameState& s)
{
cout<< "Turn "<< turn++ << " taken by player"
<< s.current_player << "\n" ;
s.current_player = (s.current_player + 1) % s.number_of_players;
s.winning_player = s.current_player;
}
auto have_winner = [&](GameState& s)
{
return turn == max turns;
};
请注意,我们定义了一些额外的lambda函数将会使用到的状态(与模拟游戏相关)。完成这些定义以后,就可以调用模板方法了:
cpp
run_game(state, start, take_turn, have_winner);
此程序的输出与前面完全一样。
5.总结
优点
1.代码复用:模板方法模式允许你定义一个算法的基本步骤,并且允许子类在不改变算法结构的情况下重定义某些步骤。这样可以避免子类代码中重复的代码,提高了代码的复用性。
2.封装不变部分:模板方法模式封装了算法的不变部分,使得算法的核心逻辑得到保护不会被错误地修改。
3.扩展性:由于算法的核心逻辑已经被封装在模板方法中,因此当需要增加新的步骤或者修改某些步骤时,只需要在相应的子类中实现或修改对应的方法,而不需要修改模板方法,具有良好的扩展性。
4.行为由父类控制:子类通过实现抽象父类中方法来改变父类的行为。父类定义了执行算法的框架,子类负责具体的实现。
缺点
1.子类对父类的依赖:模板方法模式要求子类实现父类定义的方法,这可能导致子类对父类的强依赖,降低了子类的独立性。
2.可能导致大量的子类:由于模板方法模式允许子类对算法的不同部分进行定制,这可能导致为每个定制的需求都需要创建一个新的子类,从而增加了系统的复杂性。
3.对继承的过度使用:模板方法模式通过继承来实现代码的复用,但过度使用继承可能会导致系统结构复杂,维护困难。在设计时应该考虑是否有其他的设计模式可以替代继承来实现代码的复用。
4.可能违反单一职责原则:在某些情况下,模板方法可能违反了单一职责原则,因为它在父类中定义了算法的多个步骤,而这些步骤可能真有不同的职责。
总的来说,模板方法模式在需要定义算法的不变部分,并允许子类对算法的部分步骤进行定制时非常有用。但在使用时,需要权衡其优点和缺点,避免过度依赖继承和创建过多的子类。