目录
模板方法模式是一种基于继承的代码复用的行为型模式;在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。本篇博客我们一起来学习模版方法模式。
代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。
定义
- 定义了一个操作中的算法的骨架,而将部分步骤的实现在子类中完成。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。简单地说,该模式就是将子类中共有的方法放到父类中。
- 模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。没有关联关系。 因此,在模板方法模式的类结构图中,只有继承关系。
- 抽象模板(Abstract ):是一个抽象类。抽象类定义了若干个方法以表示算法的各个步骤,这些方法中有抽象方法也有非抽象方法,这些方法统称为基本方法(Primitive Operation)。重要的一点是,抽象模板中还定义了一个称之为模板方法的方法( 一般是一个具体方法 ),该方法不仅可以调用抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法,即模板方法定义了算法的骨架。
- 具体模板(Concrete ):具体类是抽象类的子类,用于实现其父类中的抽象方法来完成子类特定算法的步骤,也可以覆盖父类中已经实现的具体基本操作。
对于模板方法模式,父类提供的构建步骤和顺序或者算法骨架,通常是不希望甚至是不允许子类去覆盖的,所以在某些场景中,可以直接将父类中提供骨架的方法声明为final类型。
优点
- 提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中,去除了子类中的重复代码。- 提高了拓展性 ,封装了不变部分,扩展了可变部分 。一般来说,抽象类中的模版方法是不易反生改变的部分,而抽象方法是容易反生变化的部分,因此通过增加实现类一般可以很容易实现功能的扩展,符合开闭原则。
- 提取公共代码,便于维护. 对于模版方法模式来说,正是由于他们的主要逻辑相同,才使用了模版方法,假如不使用模版方法,任由这些相同的代码散乱的分布在不同的类中,维护起来是非常不方便的。
- 比较灵活。因为有钩子方法,因此,子类的实现也可以影响父类中主逻辑的运行。但是,在灵活的同时,由于子类影响到了父类,违反了里氏替换原则,也会给程序带来风险。这就对抽象类的设计有了更高的要求。
- 实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 & 符合“开闭原则”
缺点
- 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大
使用场景
- 一次性实现一个算法的不变部分,并将可变的行为留给子类去实现。
- 各个子类中公共的行为应该被提取出来并且集中到一个公共的父类中去,这样避免了代码的重复。
- 控子类的扩展。模板方法只在特定点调用操作,这样就只允许在这些点进行扩展。
- 在多个子类拥有相同的方法,并且这些方法逻辑相同时,可以考虑使用模版方法模式。在程序的主框架相同,细节不同的场合下,也比较适合使用这种模式。
举一个去餐馆吃饭的例子:
有两个人(小红和小明)去吃饭,首先定义这两个人的抽象类:
class Person
{
protected:
//主食。
virtual void eatSonething() = 0;
//饮料。
virtual void drinkSomthing() = 0;
public:
virtual ~Person() = default;
//开餐。
virtual void eat() = 0;
};
小红的定义如下:
class PersonHong final : public Person
{
protected:
void eatSonething()override
{
cout << "一碗面条!!! " << endl;
}
void drinkSomthing()override
{
cout << "一杯果汁!!! " << endl;
}
public:
void eat() override
{
eatSonething();
drinkSomthing();
}
};
小明的定义如下:
class PersonMing final : public Person
{
protected:
void eatSonething()override
{
cout << "一份盖饭!!! " << endl;
}
void drinkSomthing()override
{
cout << "一瓶雪碧!!! " << endl;
}
public:
void eat() override
{
eatSonething();
drinkSomthing();
}
};
客户端调用:
int main()
{
Person *xiaoHong = new PersonHong;
Person *xiaoMing = new PersonMing;
cout << "小红点餐: " << endl;
xiaoHong->eat();
cout << "\n小明点餐: " << endl;
xiaoMing->eat();
cout << endl;
delete xiaoHong;
delete xiaoMing;
xiaoHong = xiaoMing = nullptr;
system("pause");
return 0;
}
运行截图后:
这里我们会发现,小红和小明的行为中都有 eat()
方法,这时我们就很容易地想到使用继承机制,将 eat()
方法放到 Person
类中:
class Person
{
protected:
//主食。
virtual void eatSonething() = 0;
//饮料。
virtual void drinkSomthing() = 0;
public:
virtual ~Person() = default;
//开餐。
virtual void eat() final // C++ 中 final 修饰词放在函数的后面,只可以是虚函数。 加final 防止子类对其进行重写。
{
eatSonething();
drinkSomthing();
}
};
class PersonHong final : public Person // 小红
{
protected:
void eatSonething()override
{
cout << "一碗面条!!! " << endl;
}
void drinkSomthing()override
{
cout << "一杯果汁!!! " << endl;
}
};
class PersonMing final : public Person // 小明
{
protected:
void eatSonething()override
{
cout << "一份盖饭!!! " << endl;
}
void drinkSomthing()override
{
cout << "一瓶雪碧!!! " << endl;
}
};
int main()
{
Person *xiaoHong = new PersonHong;
Person *xiaoMing = new PersonMing;
cout << "小红点餐: " << endl;
xiaoHong->eat();
cout << "\n小明点餐: " << endl;
xiaoMing->eat();
cout << endl;
delete xiaoHong;
delete xiaoMing;
xiaoHong = xiaoMing = nullptr;
system("pause");
return 0;
}
这样,就已经无意识地使用了模板方法模式。模板方法模式就是这么简单。
使用模板方法模式有两点需要注意:
- 抽象模板中的方法尽可能地使用 protected 进行修饰,符合迪米特法则。
- 模板方法要加上 final 修饰词,防止子类对其进行重写。
当然,模板方法模式也可以稍作扩展,下面是利用钩子函数进行扩展的一种形式。
钩子函数:
- 由外界条件的改变,影响到模版方法的执行,这种方法就叫做钩子函数(Hook Method)。有了钩子函数,我们就可以更灵活的控制模版方法的执行了。
模板方法可以对其进行扩展,在通常的情况下,我们子类是实现父类,并不能影响父类的行为,然而使用钩子函数,我们可以让子类影响父类的行为。
例如,一般在每次点餐前可能需要一包餐巾纸,那么在 Person 类中就需要增加选餐巾纸的行为( getPaper() ),但是小明并不需要,这样我们就需要增加一个 bool 类型属性,用来判断是否需要餐巾纸,修改抽象类如下:
class Person
{
private:
//餐巾纸。
void paper()
{
cout << "需要餐巾纸!!! " << endl;
}
protected:
//主食。
virtual void eatSonething() = 0;
//饮料。
virtual void drinkSomthing() = 0;
bool m_needPaper = true;
public:
virtual ~Person() = default;
virtual void setNeedPaper(bool needPaper) = 0;
//开餐。
virtual void eat() final // C++ 中 final 修饰词放在函数的后吗,只可以是虚函数。 加final 防止子类对其进行重写。
{
eatSonething();
drinkSomthing();
if (m_needPaper) // 是否需要餐巾纸
{
paper();
}
}
};
其中 eat () 方法就是一个模版方法,它是所有子类公共的部分代码,且定义了一系列基本方法的执行顺序(执行逻辑)。而 drinkSomthing()、eatSonething() ... 等方法便是一系列子类去实现的基本方法,例如不同的Person会有不同的饮食、等等。
同样的,小明和小红也需要做相应的更改:
class PersonHong final : public Person //小红
{
protected:
void eatSonething()override
{
cout << "一碗面条!!! " << endl;
}
void drinkSomthing()override
{
cout << "一杯果汁!!! " << endl;
}
public:
void setNeedPaper(bool needPaper)override
{
m_needPaper = needPaper;
}
};
class PersonMing final : public Person // 小明
{
protected:
void eatSonething()override
{
cout << "一份盖饭!!! " << endl;
}
void drinkSomthing()override
{
cout << "一瓶雪碧!!! " << endl;
}
public:
void setNeedPaper(bool needPaper)override
{
m_needPaper = needPaper;
}
};
客户端代码设置是否需要餐巾纸:
int main()
{
Person *xiaoHong = new PersonHong;
Person *xiaoMing = new PersonMing;
cout << "小红点餐: " << endl;
xiaoHong->eat();
cout << "\n小明点餐: " << endl;
xiaoMing->setNeedPaper(false);
xiaoMing->eat();
cout << endl;
delete xiaoHong;
delete xiaoMing;
xiaoHong = xiaoMing = nullptr;
system("pause");
return 0;
}
这样的话,我们就可以通过更改子类进而影响父类中的行为,虽然父类中的行为会随子类而改变,然而行为的框架却是没有变,符合模板方法模式的定义。