文章目录
- 1. 设计模式分类
- 1.1 GOF-23 模式分类
- 1.2 从封装变化角度对模式分类
- 2. 重构(使用模式的方法)
- 2.1 重构获得模式 Refactoring to Patterns
- 2.2 重构关键技法
- 3. “组件协作”模式
- 4. Template Method 模式
- 4.1 动机( Motivation)/应用场景
- 4.1.1 结构化软件设计流程
- 4.1.2 面向对象软件设计流程
- 4.1.3 对比两种写法:
- 4.2 早绑定和晚绑定
- 4.3 模式定义
- 4.4 结构( Structure)
- 4.5 要点总结
上篇介绍了面向对象设计的原则和目标之后,本篇将会介绍非常经典,并且具有示范效应的模式-模板方法Template Method。Template Method模式是一种
非常基础性
的设计模式,在面向对象系统中有着大量的应用。它用
最简洁的机制(虚函数的多态性)
为很多应用程序框架提供了灵活的
扩展点(继承+多态)
,是代码复用方面的基本实现结构。(
只要写过面向对象的应用程序,一定用过Template Method,只是可能没有写过核心流程)
1. 设计模式分类
1.1 GOF-23 模式分类
首先看一下,在《设计模式:可复用面向对象软件的基础》中对23种设计模式整体有如下分类方法:
- 从目的来看:
- 创建型( Creational) 模式: 将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象
创建时
具体类型实现引来的冲击。 - 结构型( Structural) 模式: 通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击。
- 行为型( Behavioral) 模式 : 通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
- 创建型( Creational) 模式: 将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象
- 从范围(实现手段)来看
- 类模式处理类与子类的静态关系:更偏重于继承方案
- 对象模式处理对象间的动态关系:更偏重于组合方案
1.2 从封装变化角度对模式分类
在实践中总结的一种分类方式如下:
- 组件协作:解决协作问题
- Template Method
- Observer / Event
- Strategy
- 单一职责:解决类与类之间责任划分的问题
- Decorator
- Bridge
- 对象创建:解决对象创建过程中的依赖关系
- Factory Method
- Abstract Factory
- Prototype
- Builder
- 对象性能:
- Singleton
- Flyweight
- 接口隔离:
- Façade
- Proxy
- Mediator
- Adapter
- 状态变化:
- Memento
- State
- 数据结构:
- Composite
- Iterator
- Chain of Resposibility
- 行为变化:
- Command
- Visitor
- 领域问题:
- Interpreter
2. 重构(使用模式的方法)
2.1 重构获得模式 Refactoring to Patterns
学习设计模式中非常重要的方法
-
面向对象设计模式是
“好的面向对象设计”
,所谓“好的面向对象设计”指是那些可以满足“应对变化,提高复用”
的设计 。 -
现代软件设计的特征是“需求的频繁变化”。设计模式的要点是“
寻找变化点,然后在变化点处应用设计模式
,从而来更好地应对需求的变化” 。“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。 -
设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提倡的“Refactoring to Patterns” 是目前普遍公认的最好的使用设计模式的方法。
对本条的理解:没有使用模式的情况下,代码的结构关系是怎样的,存在什么样的问题,违背了怎样的设计原则,通过迭代重构的方式去修正他,通过修正得到了一种良好的解决方案,得到一种模式。推荐在工作中也是采用这种方式来得到一种模式,除非你已经在该领域有丰富的经验,从而对模式的使用很有把握。 -
推荐图书
这两本书代码和思想是与后期博文介绍的思想都是很类似的。
2.2 重构关键技法
以下的五种重构技巧,目前的理解可能还不够到位,但是原则很重要,技法也是很重要。
-
静态 -> 动态
-
早绑定 -> 晚绑定
-
继承 -> 组合
-
编译时依赖 -> 运行时依赖
-
紧耦合 -> 松耦合
其实上面五种技巧讲的是一件事情,也可以看做是从不同角度看待同一个问题。
3. “组件协作”模式
-
现代软件专业分工之后的第一个结果是
“框架与应用程序的划分”
,“组件协作”模式通过晚期绑定
,来实现框架与应用程序之间的松耦合
,是二者之间协作时常用的模式。 -
典型模式
- Template Method 模板方法
- Strategy 策略模式
- Observer / Event 事件模式
当我们介绍这三种模式是“组件协作”模式时,并不是说其他模式与“组件协作”模式没有关系,其实也有关系,只是上述三种模式体现的最为强烈,特征表现特别明显。
4. Template Method 模式
4.1 动机( Motivation)/应用场景
-
在软件构建过程中,对于某一项任务,它常常有
稳定
的整体操作结构,但各个子步骤却有很多改变
的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。 -
如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
4.1.1 结构化软件设计流程
有以下代码,程序库开发人员开发的class Library,其中包含了完成某个任务的几个步骤,假设为step1,step3,step5。
(1)template1_lib.cpp:
//程序库开发人员
class Library{
public:
void Step1(){
//...
}
void Step3(){
//...
}
void Step5(){
//...
}
};
作为应用程序开发人员,为了实现功能,class Application中也做了step2,step4这2个步骤,并且开发了main函数。
完成步骤的建立之后,在main函数中,以某种具体流程串起来。以下代码中线执行流程为:先执行lib.Step1
,再根据app.Step2
的返回值来执行lib.Step3
,再重复执行app.Step4
四次,最后执行lib.Step5
(2)template1_app.cpp:
//应用程序开发人员
class Application{
public:
bool Step2(){
//...
}
void Step4(){
//...
}
};
int main()
{
Library lib();
Application app();
lib.Step1();
if (app.Step2()){
lib.Step3();
}
for (int i = 0; i < 4; i++){
app.Step4();
}
lib.Step5();
}
博文中展示的代码是没有遵循C++的编码规范的。
4.1.2 面向对象软件设计流程
还有第二种做法
程序库开发人员开发的class Library,除了包含step1,step3,step5,同时也将step2,step4写下,但是不做实现。
大家在以前的开发中经常会碰到代码样例,早期做windows程序开发时,微软会推荐先做windows典型应用程序的流程,会提供类似的样例代码,说明需要做什么步骤,哪些可以直接调用,就像step1,step3,step5,哪些需要自己去写,例如step2,step4。
其实框架人员已经开发好了整体流程,常常是不需要更改,也就是稳定的,所以框架开发人员或者说程序库开发人员,完全可以将流程写下来,这里的流程是和上面代码表达的流程是一样的,只不过是由框架开发人员去写。
(1)template2_lib.cpp:
//程序库开发人员
class Library{
public:
//稳定 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; //变化
};
(2)template2_app.cpp:
子类作为应用程序开发人员,重写Step2,Step4。
在Library* pLib=new Application()
中pLib
是多态指针,其声明类型为Library,实际类型为Application,当他调用虚函数的时候,就会按照虚函数动态绑定的规则去调用。
lib->Run()
是非虚函数,但是其里面Step2,Step4是虚函数,因此其会按照虚函数的调用规则去找子类Application的实现。
//应用程序开发人员
class Application : public Library {
protected:
virtual bool Step2(){
//... 子类重写实现
}
virtual void Step4() {
//... 子类重写实现
}
};
int main()
{
Library* pLib=new Application();
lib->Run();
delete pLib;
}
}
细节:
virtual ~Library(){ }
:在C++中写一个基类,有一条原则就是将基类中的析构函数写成虚的,这样就可以在delete pLib
调用到子类的析构函数
4.1.3 对比两种写法:
方法一 是一种结构化软件设计流程,其结构图如下:
方法二 是面向对象软件设计流程,其结构图如下:
在方法二中将程序主流程写到了Library中,应用程序就相对写的少了。而且可以看到方法一中是蓝色框调用红色框,而方法二中是红色框调用蓝色框。
4.2 早绑定和晚绑定
对上述方法进行梳理可以看到第一种方法是一种早绑定的方法,因为Library天然是写的早的,Application写的晚,利用晚的东西调用早的东西就是早绑定,这是编程语言默认的做法。
但是在面向对象软件语言以后,Library还是写的早的,Application还是写的晚,而使用Library调用Application,这样就是晚绑定。
4.3 模式定义
GoF中对模板方法模式的定义如下:
定义一个操作中的算法的骨架(对应第二种方法的run函数) (稳定
),而将一些步骤延迟(延迟一般代表定义一个虚函数,让子类去实现虚函数,也就是支持子类来变化)(变化
)到子类中。 Template Method使得子类可以不改变(复用
)一个算法的结构,即可重定义(override 重写)该算法的某些特定步骤。 —《设计模式》GoF
可以参考方法二中的代码进行映射理解的。以下代码深刻揭示了绝大多数设计模式的最核心的结构特点就是稳定中有变化,run()是稳定的,Step2()、Step4()是变化的,在C++语言层面体现出来的就是,稳定的代码需要写成非虚函数,要支持变化的要写成虚函数。
//稳定 template method
void Run(){
Step1();
if (Step2()) { //支持变化 ==> 虚函数的多态调用
Step3();
}
for (int i = 0; i < 4; i++){
Step4(); //支持变化 ==> 虚函数的多态调用
}
Step5();
}
那么这种模式有什么缺点?
前面假定Run()是稳定的,但是假如Run()不稳定了,也就不适合使用Template Method,因此该设计模式使用的前提就是Run()是稳定的。
当软件体系结构中所有都不稳定的时候,任何一种模式都不可使用,这是因为设计模式假设条件是必须有一个稳定点。
反过来,当Step2()、Step4()都是稳定的时候,设计模式也就没有使用的意义。
设计模式最大的作用就是变化和稳定之间寻找隔离点,然后来分离他们,从而来管理变化
,按照compact的讲法(不懂什么一次),就是将变化像小兔子一样关进笼子,让其在笼子里跳,而不至于跳出来将整个房间污染。所以正常的软件结构,一定是既有变化又有稳定点的。
在模式应用的时候,核心就是分辨出来软件体系结构中,哪些是稳定的,哪些是变化的。
有了这个意识之后,再看下图,程序主流程Run()为什么放在红框中,这是因为我们假定他是想对稳定的,更具有复用价值。
绝大对数软件框架中都有Template Method模式,相对于第一种方法,使用Template Method模式,应用程序的核心流程是塞到父类里,所以应用程序开发人员是看不到变化过程,只需要写几个步骤就可以了。
所以在面向对象的Template Method模式下,如果你是application的开发人员,经常会面对一种“只见树木,不见森林”的困惑,因为你只是在写子步骤,而没有去写核心流程。
4.4 结构( Structure)
上图是《设计模式》GoF中定义的Template Method的设计结构。结合上面的代码看图中最重要的是看其中稳定和变化部分,也就是下图中红框和蓝框框选的部分。
看任何设计模式的时候,包括其类图,都画一画哪些是稳定的,哪些是变化的,当你形成了这样一种习惯之后,你对模式的理解会更上一层楼,而不是只看其代码关系。
4.5 要点总结
-
Template Method模式是一种
非常基础性
的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)
为很多应用程序框架提供了灵活的扩展点(继承+多态)
,是代码复用方面的基本实现结构。(只要写过面向对象的应用程序,一定用过Template Method,只是可能没有写过核心流程) -
除了可以灵活应对子步骤的变化外,
“不要调用我,让我来调用你”
的反向控制结构是Template Method的典型应用。- 在Template Method设计之前,软件体系架构的主流是应用程序开发人员来调用Library开发人员写的代码,当面向对象的设计模式成为主流之后,调用关系反转,让早写的来调用晚写的,而依靠的机制就是虚函数的机制,也就是虚函数的晚绑定机制
- 虚函数是面向对象里面最核心的晚绑定机制,但是任何一个编程语言的晚绑定机制不只有虚函数,像C++中还有函数指针,但是函数指针在某些场合不具有虚函数的抽象性
-
在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法(前后有流程环境才有意义)。