本文先介绍了三种不同的设计模式分类方法,比如按照目的、按照范围、按照封装变化角度不同;之后介绍了敏捷软件开发中普遍使用Refactoring to Patterns的方法对软件进行重构,介绍了两本重构书籍,与几种重构方法,核心就是晚绑定;再然后介绍了“组件协作”模式解决的问题,以及经典的三种设计模式,其中Template Method是本文重点;再然后对模板方法这一设计模式进行详细的介绍,包括:动机、代码实例、模式类图、模式定义,再结合代码实例,比较了结构化软件设计和面向对象软件设计,并对早绑定与晚绑定进行比较;最后对Template Method这一设计模式进行了总结。
最后感谢GeekBand的李建忠老师、GOF等前辈
1. GOF-23 模式分类
-
从目的来看:
- 创建型(Creational)模式:将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。
- 结构型(Structural)模式:通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击。
- 行为型(Behavioral)模式:通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
-
从范围来看:
- 类模式处理类与子类的静态关系(继承)。
- 对象模式处理对象间的动态关系(依赖、关联)。
-
从封装变化角度:
- 须知:采用如下的分类方法不是说其它的分类下的模式和当前模式没有任何联系,而是每个分类下列举的模式是最能反映当前主题的经典模式。
- 组件协作:
- 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. 重构获得模式(Refactoring to Patterns)
- 面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”指是那些可以满足 “应对变化,提高复用”的设计 。
- 现代软件设计的特征是“需求的频繁变化”。设计模式的要点是“寻找变化点,然后在变化点处应用设计模式,从而来更好地应对需求的变化”.“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。
- 设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提倡的“Refactoring to Patterns”是目前普遍公认的最好的使用设计模式的方法。
3. 推荐书籍
4. 重构关键技法
- 静态 ——》 动态
- 早绑定 ——》 晚绑定
- 继承 ——》 组合
- 编译时依赖 ——》 运行时依赖
- 紧耦合 ——》 松耦合
- 补:其实这些技法所展现得都是同一个内容,只不过是站在了不同的角度看待而已。
5. “组件协作”模式
- 现代软件专业分工之后的第一个结果是“框架与应用程序的划分”[框架就是稳定软件结构,应用程序就是具体的业务处理方法],“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
- 典型模式
- Template Method(今天主要简述这个模式)
- Observer / Event
- Strategy
6. 动机(Motivation)
- 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
- 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
7. 代码展示
template1_lib.cpp
与template1_cpp.cpp
为未使用设计模式(结构化软件常用的设计方法)的“应用程序开发人员”与“程序库开发人员”协作方式。其中程序库开发人员在早期就设计好了库函数,比如Library
中的Step1()
、Step3()
、Step5()
;之后应用程序开发人员在Application
中添加了新函数Step2()
、step4()
,并在main()
中编写了框架(执行流程),依次调用了Step1()
、Step2()
、Step3()
、Step4()
、Step5()
。
// template1_lib.cpp
// 程序库开发人员
class Library{
public:
void Step1(){
//...
}
void Step3(){
//...
}
void Step5(){
//...
}
};
// template1_cpp.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();
}
template2_lib.cpp
与template2_cpp.cpp
实现的是面向对象软件设计下的“程序库开发人员”与“应用程序开发人员”协同工作代码,和结构化软件设计不同的是,程序库开发人员直接实现了框架Run()
、实现了诸如Step1()
、Step3()
、Step5()
等稳定方法,并且为变化的方法Step2()
、Step4()
提供了晚绑定机制(虚函数);应用程序开发人员在参与开发时只需要继承Library
并实现程序库不确定的方法Step2()
、Step4()
即可完成开发,而不需要再编写框架。
// 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; //变化
};
// template2_cpp.cpp
// 应用程序开发人员
class Application : public Library {
protected:
virtual bool Step2(){
//... 子类重写实现
}
virtual void Step4() {
//... 子类重写实现
}
};
int main()
{
Library* pLib = new Application();
lib->Run();
delete pLib;
}
需要注意的是:
Library
中的Run()
是相对稳定的方法,不要定义为虚函数;Library
中定义的Step
系列方法都是Run()
的执行步骤,因此设计为protected
访问权限;- 对于稳定的方法直接在
Library
中实现,对于不确定的方法,在Library
中定义虚函数或者纯虚函数,延迟到子类Application
中实现; - 在
Run()
中我们可以调用普通函数和虚函数来确定模型的基本流程,虚函数最终会调用扩展的代码; - 由于在
Library
中我们已经确定了框架,因此在Application
中直接重写相关代码即可,而不需要再写框架了。
8. 结构化软件设计流程
9. 面向对象软件设计流程
10. 早绑定 vs 晚绑定
早绑定与晚绑定的本质:
- 早绑定:之后写的代码调用之前写的代码
- 晚绑定:之前写的代码调用之后写的代码
11. 模式定义
定义一个操作中的算法的骨架 (稳定)[如果没有一个稳定的算法骨架,则不适合使用Template Method设计模式],而将一些步骤延迟(变化)到子类中[Step2()与Step4()方法在Application中实现]。Template Method使得子类可以不改变(复用)一个算法的结构[Run()方法在Library中的实现]即可重定义(override 重写)该算法的某些特定步骤[Step2()与Step4()重定义]。
设计模式最大的作用就是在稳定和变化之间寻找隔离点,然后来分离它们,从而来管理变化,就像我们使用笼子把兔子关起来,兔子在笼子中蹦来蹦去,但不会弄乱我们的房间。但是我们不应将设计模式应用到两种极端的情况:完全稳定和完全不稳定之下,当这两种情况发生时,设计模式失去意义。所以在程序设计中需要分清那些是稳定的那些是变化的。
当然我们所说的稳定也不是绝对的稳定,而是相对的,比如Step2()
与Step3()
方法的变化频率为一天,而Run()
方法的变化频率为一年,那么Run()
相对于Step2()
与Step3()
就是稳定的。一段代码稳定的本质就是能够被复用。
如果你是一个应用程序开发人员,你的心中可能都有这种困惑:“只见树木,不见森林”,我们只需要继承父类,重写虚函数,甚至都不用写main()
方法,但是我们的代码会被正确的调用。其实底层就是采用的Template Method设计模式,早早的为软件设计好了框架。
12. 模型结构
一个很好的建议就是,对于每一种设计模式,我们首先要理解的不是这种设计模式的详细代码,而是去理解这个模式(尤其类图)中那些部分是变化的,那些部分是稳定的,理解模型中稳定与变化的力量,可以使得我们对设计模式的理解更上一层楼。比如在上述类图中,AbstarctClass
中的TemplateMethod()
就是稳定的,相当为Library
中的Run()
、Step1()
、Step3()
、Step5()
,而PrimitiveOperation1()
与PrimitiveOperation2()
是变化的,相当于Application
中的Step2()
、Step4()
,因此我们就需要使用子类ConcreteClass
与Application
去重写,从而实现变化的隔离。
13. 要点总结
- Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展[通过继承、重写虚函数实现扩展]点,是代码复用方面的基本实现结构。
- 除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用你”的反向控制结构是Template Method的典型应用。
- 在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法[因为这些方法一般是作为流程的一部分,使用public抛出并没有意义]。