设计模式2:“组件协作”模式——Template Method、Strategy、 Observer / Event
“组件协作”模式:
- 现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
- 典型模式:Template Method、Strategy、 Observer / Event。
90年代初期-也就是OO发展的早期-我们都深深受继承的观念所吸引。有了继承,我们就可以运用差异编程(program by difference)来设计程序!也就是说给予某个 class,它所完成的事大部分都是我们用得着的,那么我们就可以建立一个subclass,并只改变我们不想要的一小部分。只要继承 class 就可以再利用程序代码!
到了 1995 年,继承非常遭到滥用,而且代价昂贵。Gamma, Helm, Johnson,Vlissides 甚至强调「多使用 object composition 而少使用 class inheritance」。应该可以减少 inheritance 的使用,经常以 composition 或 delegation 来取代。
有两个 patterns 可以归纳 inheritance 和 delegation 之间的差异:Template Method 和 Strategy。两者都可以解决从detailed context(细节化脉络/情境)中分离出generic algorithm(一般化算法)的问题,这种需求在软件设计中极为常见。是的,我们有一个用途广泛的 algorithm,为了符合DIP(Template Method 让一个 generic algorithm 可以操纵(manipulate)多个可能的detailed implementations,而 Strategy 让每一个 detailed implementation 都能够被许多不同的 generic algorithms 操纵(manipulated)。
ref 《Agile Software Development, Principles, Patterns, and Practices》
Template Method模式
动机(Motivation)
- 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很大改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
- 如何在确定稳定操作结构额前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
下面以具体的代码为例进行学习,见代码 template1:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //程序库开发人员 2 class Library{ 3 public: 4 void Step1(){ 5 //... 6 } 7 void Step3(){ 8 //... 9 } 10 void Step5(){ 11 //... 12 } 13 }; 14 15 //应用程序开发人员 16 class Application{ 17 public: 18 bool Step2(){ 19 //... 20 } 21 void Step4(){ 22 //... 23 } 24 }; 25 int main() 26 { 27 Library lib(); 28 Application app(); 29 lib.Step1(); 30 if (app.Step2()){ 31 lib.Step3(); 32 } 33 for (int i = 0; i < 4; i++){ 34 app.Step4(); 35 } 36 lib.Step5(); 37 }
上述代码,Library开发人员需要开发1、3、5三个步骤,Application开发人员开发2、4两个步骤和程序主流程。这种通过应用端对流程框架进行调用的方式,称之为早绑定。
上述代码如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求呢?
请看template2代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //template2_lib.cpp 2 //程序库开发人员 3 class Library{ 4 public: 5 //稳定 template method 6 void Run(){ 7 8 Step1(); 9 10 if (Step2()) { //支持变化 ==> 虚函数的多态调用 11 Step3(); 12 } 13 14 for (int i = 0; i < 4; i++){ 15 Step4(); //支持变化 ==> 虚函数的多态调用 16 } 17 18 Step5(); 19 20 } 21 virtual ~Library(){ } 22 23 protected: 24 25 void Step1() { //稳定 26 //..... 27 } 28 void Step3() {//稳定 29 //..... 30 } 31 void Step5() { //稳定 32 //..... 33 } 34 35 virtual bool Step2() = 0;//变化 36 virtual void Step4() =0; //变化 37 }; 38 39 40 ////template2_app.cpp 41 //应用程序开发人员 42 class Application : public Library { 43 protected: 44 virtual bool Step2(){ 45 //... 子类重写实现 46 } 47 48 virtual void Step4() { 49 //... 子类重写实现 50 } 51 }; 52 53 54 int main() 55 { 56 Library* pLib=new Application(); 57 lib->Run(); 58 59 delete pLib; 60 } 61 }
上述代码在template1的基础上做修改后,Library开发人员需要开发1、3、5三个步骤和程序主流程,Application开发人员只需开发2、4两个步骤。这种将流程放入开发库,由库开发者开发,提供给应用程序开发者调用的方法,称之为晚绑定。
在代码template1中,存在以下两个问题:
第一,对应用程序开发人员来说,需要自己开发其中的2、4两个步骤,要求比较高,需要对库中的函数情况比较了解,而且重写的两个函数的难度也相对较大。
第二,库开发人员和应用程序开发人员开发的内容的耦合度很高,需要由用开发人员来组织整体调用流程,未来程序的扩展性和可维护性的难度都比较大。
而在代码template2中,库开发人员对开发库进行了重构,增加量两个虚函数,定义了一个run函数,将之前的主流程放入,从而使流程稳定。同时库开发者不知道应用程序开发者会如何设计2、4步骤,因此将其定义为虚函数。
应用程序开发者只需在子类中重新定义这两个虚函数即可,不需要再去考虑整个流程的实现,有效的降低了开发难度。
模式定义
定义一个操作中的算法的骨架(稳定),而将一些步骤延迟(变化)到子类中。Template Method 使得子类可以在不改变(复用)一个算法的结构即可重新定义(Override 重写)该算法的某些特定步骤。
——《设计模式》GoF
Template Method 案例(MFC)
在word,excel,ppt等软件中,打开一个文档的过程是相似的,设计一个共有基类实现这个流程,让不同的文档类继承,实现具体的打开操作。
《深入浅出MFC》p.84:CDocument::OnFileOpen中所调用的Serialize 是哪一个class 的成员函数呢?如果它是一般(non-virtual)函数,毫无问题应该是CDocument::Serialize。但因这是个虚拟函数,情况便有不同。既然derived class 已经改写了虚拟函数Serialize,那么理当唤起derived class 之 Serialize函数。这种行为模式非常频繁地出现在application framework 身上。
案例代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include <iostream> 2 using namespace std; 3 4 // 这里扮演application framework 的角色 5 class CDocument 6 { 7 public: 8 void OnFileOpen() // 此即所谓Template Method 9 { 10 // 这是一个算法,骨干已完成。每个cout输出代表一个实际应有动作 11 cout << "dialog..." << endl; 12 cout << "check file status..." << endl; 13 cout << "open file..." << endl; 14 Serialize(); // 唤起哪一个函数? 15 cout << "close file..." << endl; 16 cout << "update all views..." << endl; 17 } 18 19 virtual void Serialize() { }; 20 }; 21 22 // 以下扮演 application 的角色 23 class CMyDoc : public CDocument 24 { 25 public: 26 virtual void Serialize() 27 { 28 // 只有应用程序员自己才知道如何读取自己的文件档 29 cout << "CMyDoc::Serialize()" << endl; 30 } 31 }; 32 33 int main() 34 { 35 CMyDoc myDoc; // 假设主窗口菜单的[File/Open]被按下后执行至此。 36 myDoc.OnFileOpen(); 37 }
输出结果如下:
dialog...
check file status...
open file...
CMyDoc::Serialize()
close file...
update all views...
Template Method 结构
要点总结
- Template Method 是一种非常基础性的设计模式,在面向对象的系统中,有着大量的应用。他用最简洁的机制(虚函数的多态性)为很多应用程序的框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
- 除了可以灵活对应子步骤的变化外,“不要调用我,让我来调用你”的反向控制结构是 Template Method 的典型应用。
- 在具体实现方面,被 Template Method 调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),一般推荐将他们设置为 Protected 方法。
动机(Motivation)
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。(分枝:大量不用代码加载到代码段里)
如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //strategy1.cpp 2 enum TaxBase { 3 CN_Tax, 4 US_Tax, 5 DE_Tax, 6 FR_Tax //更改 7 }; 8 9 class SalesOrder{ 10 TaxBase tax; 11 public: 12 double CalculateTax(){ 13 //... 14 15 //用条件判断语句 16 if (tax == CN_Tax){ 17 //CN*********** 18 } 19 else if (tax == US_Tax){ 20 //US*********** 21 } 22 else if (tax == DE_Tax){ 23 //DE*********** 24 } 25 else if (tax == FR_Tax){ //更改 26 //... 27 } 28 29 //.... 30 } 31 }; 32 33 // 更改违背开闭原则, 应采用扩展模式 34 // 复用:二进制的编译复用,编译单位的复用,而不是代码的复制粘贴
上述代码,当有新的需求变动,如增加一个新的税种(FR_Tax ),开发人员需要在枚举enum TaxBase 中添加FR_Tax,在计算税务的函数CalculateTax中添加新的分支流程。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //strategy2.cpp 2 class TaxStrategy{ 3 public: 4 virtual double Calculate(const Context& context)=0; 5 virtual ~TaxStrategy(){} 6 }; 7 8 9 class CNTax : public TaxStrategy{ 10 public: 11 virtual double Calculate(const Context& context){ 12 //*********** 13 } 14 }; 15 16 class USTax : public TaxStrategy{ 17 public: 18 virtual double Calculate(const Context& context){ 19 //*********** 20 } 21 }; 22 23 class DETax : public TaxStrategy{ 24 public: 25 virtual double Calculate(const Context& context){ 26 //*********** 27 } 28 }; 29 30 31 //扩展 32 //********************************* 33 class FRTax : public TaxStrategy{ 34 public: 35 virtual double Calculate(const Context& context){ 36 //......... 37 } 38 }; 39 40 class SalesOrder{ 41 private: 42 TaxStrategy* strategy; 43 44 public: 45 SalesOrder(StrategyFactory* strategyFactory){ 46 this->strategy = strategyFactory->NewStrategy(); 47 } 48 ~SalesOrder(){ 49 delete this->strategy; 50 } 51 52 public double CalculateTax(){ 53 //... 54 Context context(); 55 56 double val = 57 strategy->Calculate(context); //多态调用 58 //... 59 } 60 61 };
上述代码 ,开发人员提取出抽象基类,增加虚函数Calculate(),面对具体的税种时重写基类的Calculate函数,当有新的税种FR_Tax增加时,增加相应的类(实际工程中,一般每个类都放在一个单独的文件中,为了便于演示文中几个类的代码都放在了一个文件中)。主调用函数通过基类的虚函数多态调用具体的计算流程,新的需求增加时,不需要改动。
模式定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。
——《设计模式》GoF
结构(Structure)
要点总结
- Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
- Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。
- 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。
1.应用情景
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使得对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
2.解决方案
将多个算法从代码中提取出来,封装成一个个的策略类,并提供一个统一的接口作为父类。在需要使用多种算法的对象中定义一个该算法的父类指针,根据传进去的子类实现运行时确定计算方法。这样便可以在不更改对象源代码的情况下非常轻松的扩展策略算法。如果算法本身不涉及数据存储,算法类也可以使用单例模式设置,降低内存消耗。
观察者模式
动机(Motivation)
- 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” —— 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
- 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
下面以一个文件分割为例子,先看一下FileSplitter1伪代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // FileSplitter1.cpp 2 class FileSplitter 3 { 4 string m_filePath; 5 int m_fileNumber; 6 ProgressBar* m_progressBar; // 违背依赖倒置原则,应该依赖于抽象而不是细节 7 // 不一定是进度条,也可能是进度块、控制台甚至不是Windows gui。 8 // 1.尝试找基类 9 10 public: 11 FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) : 12 m_filePath(filePath), 13 m_fileNumber(fileNumber), 14 m_progressBar(progressBar){ 15 16 } 17 18 void split(){ 19 20 //1.读取大文件 21 22 //2.分批次向小文件中写入 23 for (int i = 0; i < m_fileNumber; i++){ 24 //... 25 float progressValue = m_fileNumber; 26 progressValue = (i + 1) / progressValue; 27 m_progressBar->setValue(progressValue); 28 } 29 } 30 }; 31 32 //MainForm1.cpp 33 class MainForm : public Form 34 { 35 TextBox* txtFilePath; 36 TextBox* txtFileNumber; 37 ProgressBar* progressBar; 38 39 public: 40 void Button1_Click(){ 41 42 string filePath = txtFilePath->getText(); 43 int number = atoi(txtFileNumber->getText().c_str()); 44 45 FileSplitter splitter(filePath, number, progressBar); 46 47 splitter.split(); 48 49 } 50 };
上述代码中将路径为filePath的文件分割成number 个小文件,并用进度条显示分割进度。在文件分割对象中传入ProgressBar* 参数,进行回调调用显示文件分割进度。
类FileSplitter中ProgressBar*,违背依赖倒置原则,应该依赖于抽象而不是细节;当进度显示方式改变时,也不得不更改分割方法split().
尝试寻找通知或显示或依赖于其他对象的基类,定义一个统一的接口,对于需要做出相应动作的类便可以继承该接口并实现它。更改代码如下FileSpliter2:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class IProgress{ 2 public: 3 virtual void DoProgress(float value)=0; 4 virtual ~IProgress(){} 5 }; 6 7 8 class FileSplitter 9 { 10 string m_filePath; 11 int m_fileNumber; 12 13 // ProgressBar* m_progressBar; // 具体的通知控件 14 List<IProgress*> m_iprogressList; // 抽象通知机制,支持多个观察者 15 16 public: 17 FileSplitter(const string& filePath, int fileNumber) : 18 m_filePath(filePath), 19 m_fileNumber(fileNumber){ 20 21 } 22 23 void split(){ 24 25 //1.读取大文件 26 27 //2.分批次向小文件中写入 28 for (int i = 0; i < m_fileNumber; i++){ 29 //... 30 31 float progressValue = m_fileNumber; 32 progressValue = (i + 1) / progressValue; 33 onProgress(progressValue);//发送通知 34 } 35 36 } 37 38 39 void addIProgress(IProgress* iprogress){ 40 m_iprogressList.push_back(iprogress); 41 } 42 43 void removeIProgress(IProgress* iprogress){ 44 m_iprogressList.remove(iprogress); 45 } 46 47 48 protected: 49 virtual void onProgress(float value){ 50 51 List<IProgress*>::iterator itor=m_iprogressList.begin(); 52 53 while (itor != m_iprogressList.end() ) 54 (*itor)->DoProgress(value); //更新进度条 55 itor++; 56 } 57 } 58 };
在上述代码中,去掉具体的通知控件 ProgressBar* m_progressBar,代之以抽象出接口类IProgress(c++ 无接口概念,但可以以虚基类代替,类前加I,以表示该类为接口类)。采用链表机制以接受多个通知对象(抽象通知机制,支持多个观察者),类似于c# 中委托,事件的作用。
主程序(应用程序、调用函数)代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //MainForm2.CPP 2 class MainForm : public Form, public IProgress 3 { 4 TextBox* txtFilePath; 5 TextBox* txtFileNumber; 6 7 ProgressBar* progressBar; 8 9 public: 10 void Button1_Click(){ 11 12 string filePath = txtFilePath->getText(); 13 int number = atoi(txtFileNumber->getText().c_str()); 14 15 ConsoleNotifier cn; 16 17 FileSplitter splitter(filePath, number); 18 19 splitter.addIProgress(this); //订阅通知 20 splitter.addIProgress(&cn); //订阅通知 21 22 splitter.split(); 23 24 splitter.removeIProgress(this); 25 26 } 27 28 virtual void DoProgress(float value){ 29 progressBar->setValue(value); 30 } 31 }; 32 33 // ConsoleNotifier类 cpp 34 class ConsoleNotifier : public IProgress { 35 public: 36 virtual void DoProgress(float value){ 37 cout << "."; 38 } 39 };
在上面代码,控制台和进度条都表示了文件分割进度。在代码FileSpliter2中,消除了对具体通知对象的依赖,采用虚函数机制,当新的依赖对象产生时,只需要增加相应继承类,无需更改FileSpliter类。
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
——《设计模式》GoF
案例(mfc,Java Library)
代码示例
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include <iostream> 2 #include<vector> 3 using namespace std; 4 5 class Observer 6 { 7 public: 8 virtual void update(int value) = 0; 9 }; 10 11 class Subject 12 { 13 int m_value; 14 vector<Observer*> m_views; 15 public: 16 void attach(Observer* obs) 17 { 18 m_views.push_back(obs); 19 } 20 void set_val(int value) 21 { 22 m_value = value; 23 notify(); 24 } 25 void notify() 26 { 27 for (unsigned int i = 0; i < m_views.size(); ++i ) 28 m_views[i]->update(m_value); 29 } 30 }; 31 32 class Observer1 : public Observer 33 { 34 int m_div; 35 public: 36 Observer1(Subject *model, int div) 37 { 38 model->attach(this); 39 m_div = div; 40 } 41 /* virtual */void update(int v) 42 { 43 //… 44 cout << "Observer1 update: " << v << endl; 45 } 46 }; 47 48 class Observer2 : public Observer 49 { 50 int m_mod; 51 public: 52 Observer2(Subject *model, int mod) 53 { 54 model->attach(this); 55 m_mod = mod; 56 } 57 /* virtual */void update(int v) 58 { 59 //... 60 cout << "Observer2 update: " << v << endl; 61 } 62 }; 63 64 int main() 65 { 66 Subject subj; 67 Observer1 o1(&subj, 4); 68 Observer1 o2(&subj, 3); 69 Observer2 o3(&subj, 3); 70 subj.set_val(14); 71 }
结构
要点总结
- 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
- Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。
解决方案
在软件中为定义一个发出通知的接口,对于需要接受通知的类便可以继承该接口并实现它。在发出通知的类中放置一个接口数组,在客户端程序中将所有需要接受通知的对象都放置进该数组中。这样,便可以实现通过客户程序灵活的选择和分配消息传送渠道,实现发出通知代码的复用。
总结
在任何 OOPL 中都可以创造出「固定并且代表一组无限可能的行为」的抽象概念。这些抽象概念就是 abstract base classes,而那组无限可能的行为则以所有可能的 derived classes 来表现。
模块可以操作抽象概念。由于依存于固定之抽象概念,所以模块可对修改保持封闭,而又可透过创造抽象概念之 new derived classes 对模块的行为加以扩充。
Template Method和 Strategy(Policy)是满足 OCP 的最常见手法,它们都表达了一个清晰的分离(separation)概念,将通用功能性从该功能的实现细节中分离出来。
遵循OCP 的代价是昂贵的,需要花费开发期的时间和人力来创造适当的抽象概念。这些抽象概念也增添了软件设计的复杂度,而开发人员能够承担的抽象概念数量有其极限。显然我们希望把 OCP 的应用限制在有可能发生的变更上。怎样知道哪些变更有可能发生呢?我们做适当的研究调查、提出适切的问题、运用经验与常识。最后就等变更发生!
在许多方面,OCP 是 OOD 的核心。遵循这个原则会带来 OO 技术所宣称的最大益处,亦即弹性、复用性和维护性。然而要符合此原则并非单靠使用 OOPL 就可达到,应用程序的每个部分都运用繁多的抽象化也不是个好主意。它极需仰赖开发人员只对「显示出频频改变」特性之程序部份运用抽象化解法。抗拒「草率之抽象化」和抽象化本身一样重要。
• Template Method 是利用 inheritance 来改变程序行为:在 base class 指定方法大纲,在 sub class 指定具体行为。
• Strategy 是利用 delegation 来改变程序行为:改变的不是算法局部,而是切换整个算法。
本文内容源自 :
- C++设计模式 Design Patterns 李建忠 课程
- 侯捷:设计模式讲义