组件协作模式:模板方法、策略模式、观察者模式
- 前言
模板方法
动机
- 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作
结构,但各个子步骤却有很多改变的需求,或者由于固有的原因
(比如框架与应用之间的关系)而无法和任务的整体结构同时实现 - 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变
化或者晚期实现需求
定义
- 定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟(变化)到子类中。Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override 重写)该算法的
某些特定步骤- 延迟到子类:让子类实现或override父类的virtual
- 如果没有稳定的骨架(Run)怎么办?那么前提就不成立了,就要换一种设计模式
早/晚绑定
- 反向控制结构
代码
- 稳定的函数:设非虚
- 变化的函数:设virtual
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60//程序库开发人员
class Library{
public:
// 非虚函数Run:稳定
// 虚函数:变化(step3,step4)
//稳定 template method
void Run(){
Step1();
if (Step2()) { //支持变化 ==> 虚函数的多态调用
Step3();
}
for (int i = 0; i < 4; i++){
Step4(); //支持变化 ==> 虚函数的多态调用
}
Step5();
}
virtual ~Library(){ }
protected:
void Step1() { //稳定
//.....
}
void Step3() {//稳定
//.....
}
void Step5() { //稳定
//.....
}
virtual bool Step2() = 0;//变化
virtual void Step4() =0; //变化
};
//应用程序开发人员
class Application : public Library {
protected:
virtual bool Step2(){
//... 子类重写实现
}
virtual void Step4() {
//... 子类重写实现
}
};
int main()
{
Library* pLib=new Application();
lib->Run();
delete pLib;
}
}
结构
总结
Template Method
模式是一种非常基础性的设计模式,在面向对
象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)
为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本
实现结构。- 除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用
你”的反向控制结构是Template Method
的典型应用。 - 在具体实现方面,被
Template Method
调用的虚方法可以具有实
现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将
它们设置为protected
方法(一般在父类所写的核心流程里的上下文中调用才有意义,类外调用没意义)。
策略模式
动机
- 在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担
- 如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
定义
- 定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。
什么是复用,什么是扩展/修改?
- 扩展是为了实现复用.
复用
- 拷贝粘贴不叫复用
- 不是源代码级别的复用
- 复用而是编译、部署之后,二进制级别的复用。是编译部署之后,原封不动的。
- 所以,在一堆if-else后面再加一个if-else,是修改而不是扩展
- 不是扩展(改变了此文件的源代码,需要重新编译)
- 因此没实现复用(复用之前的if-else?放屁!)
扩展
- 不改变现有类的代码,而是通过新增加一个文件来增加一个类等.
修改
修改现有类的源代码
开闭原则
- 对扩展开放
- 对修改关闭
1 | enum TaxBase { |
最终
- 扩展
- 如果有改变的话,我们新增文件FRTax.cpp , 在里面写要增加的类即可 , 其他类文件的代码不会改变 , 实现了(二进制级别的)复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59class TaxStrategy{
public:
virtual double Calculate(const Context& context)=0;
virtual ~TaxStrategy(){}
};
class CNTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class USTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class DETax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
// 扩展
// 如果有改变的话,我们新增文件FRTax.cpp , 在里面写要增加的类即可.
// 其他类文件的代码不会改变
// 实现了(二进制级别的)复用
//*********************************
class FRTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//.........
}
};
class SalesOrder{
private:
TaxStrategy* strategy; // 常用指针而非引用
public:
SalesOrder(StrategyFactory* strategyFactory){ // 工厂
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder(){
delete this->strategy;
}
public double CalculateTax(){
//...
Context context;
double val =
strategy->Calculate(context); //多态调用
//...
}
};
结构
总结
用扩展的方式,去面对需求的变化
Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换
- 运行时:多态,虚函数调用
Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式!!!
如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销.
出现很多if-else的地方,且可能需要增加改变时,就是我们需要用到策略模式的地方.
if-else : bad smile
实际上的算法远比上面的+-复杂。
补充
- 策略模式仅仅封装算法
- 方便新的算法插入到已有系统中,以及老算法从系统中“退休”,实现方法替换
- 策略模式并不决定何时使用何种算法,什么情况下使用什么算法由用户决定。
- 策略模式的重心:策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
- 算法的平等性:策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的
- 运行时策略的唯一性:运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个
- 它把采取哪一种算法或采取哪一种行为的逻辑与算法本身分离,避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展
缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
- 由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观
观察者模式
1 |
|