“组件协作”模式:现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
典型模式:Template Method、Strategy、Observer / Event。
一、模板模式(Template)
1.动机
在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因 (比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
2.作用
在确定稳定操作结构的前提下,灵活应对各个子步骤的变化或者晚期实现需求。
3.定义
定义一个操作中的算法的骨架(稳定),而将一些步骤延迟(变化)到子类中。模板模式使得子类可以不改变(复用)一个算法的结构即可重定义(重写)该算法的某些特定步骤。
4.代码
//原有代码
//程序库开发人员
class Library{
public:
void Step1(){
//...
}
void Step3(){
//...
}
void Step5(){
//...
}
};
//应用程序开发人员
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();
}
//运用模板模式后的代码
//程序库开发人员
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; //变化
};
//应用程序开发人员
class Application : public Library {
protected:
virtual bool Step2(){
//... 子类重写实现
}
virtual void Step4() {
//... 子类重写实现
}
};
int main()
{
Library* pLib=new Application();
pLib->Run();
delete pLib;
}
}
5.解析
这是运用一个Application的应用和Library的框架,共同完成一项任务。该实现有稳定的过程步骤,而具体步骤2、4存在变化性。
在原有的代码中,Library框架开发人员负责具体步骤1、3、5的实现,Application应用开发人员负责过程步骤、具体步骤2、4的实现。实现时,让Application去寻找(依赖)Library的具体步骤。但实际中,应用开发一般基于框架开发进行,即框架开发先于应用开发,且应用开发人员一般需要直接面对客户需求,即其代码常常面临变化,如果将稳定过程步骤也交给应用开发人员,会将程序中稳定和变化的部分揉在一起,不利于维护。
在运用模板模式后的代码中,稳定的过程步骤、具体步骤1、3、5由框架开发人员实现,并给出了变化步骤2、4的接口,由应用开发人员在其派生类中实现,实现了程序中稳定和变化的部分分离,使程序更加健壮。
6.结构
AbstractClass(抽象类,如Library):
定义抽象的原语操作,具体的子类将重新定义它们以实现一个算法的各步骤;
实现一个模板方法,定义一个算法骨架。
ConcreteClass(具体类,如Application):实现原语操作以完成算法中与特定子类相关的步骤。
7.总结
1.模板模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性) 为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
2. 除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用你”的反向控制结构是模板模式的典型应用,即指父类调用子类的操作。(早绑定和晚绑定)
二、策略模式(Strategy)
1.动机
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
2.作用
在运行时根据需要透明地更改对象的算法;将算法与对象本身解耦,从而避免上述问题。
3.定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。
4.代码
//原有的代码
enum TaxBase {
CN_Tax,
US_Tax,
FR_Tax //更改
};
class SalesOrder{
TaxBase tax;
public:
double CalculateTax(){
if (tax == CN_Tax){
//CN***********
}
else if (tax == US_Tax){
//US***********
}
else if (tax == FR_Tax){ //更改
// FR***********
}
}
};
//运用策略模式后的代码
class 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 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); //多态调用
}
};
5.解析
这是一个税费计算程序,而各个国家有不同的税费计算方法。现有CN、US两国的税费计算方法,由于业务扩展,该程序现需增加法国税费的计算方法。
在原有的代码中,在TaxBase枚举中增加FR的选项,并在SalesOrder:: CalculateTax()函数中扩展(if…else)语句。该方法在简单结构中看似简单,却严重违背了“开放封闭原则”的设计原则。
在运用策略模式后的代码中,不同国家对应着不同的行为(税费计算方法)。TaxStrategy是税费算法接口,具体实现在其派生类中实现。在SalesOrder类中,根据实际new出来的TaxStrategy(指针调用),在程序运行(不是编译)时调用具体的税费算法。该模式提供了一种替代继承的方法,并消除了一些条件语句,并且在需要增加/删除某国税费算法时,采用扩展方法,遵循了“开放封闭原则”的设计原则。
6.结构
Strategy(策略,如TaxStrategy): 定义所有支持的算法的公共接口。Context使用这个接口来调用某ConcreteStrategy定义的算法。
ConcreteStrategy(具体策略,如CNTax、USTax、FRTax): 以Strategy接口实现具体算法。
Context(上下文,如Context): 可定义一个接口来让Strategy来访问它的数据。
用一个ConcreteStrategy对象来配置;
维护一个对Strategy对象的引用;
7.总结
1.Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得组件在运行时方便地根据需要在各个算法之间进行切换。
2.Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。
3.如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。
三、观察者模式(Obeserver)
1.动机
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
2.作用
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
3.定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个 对象(subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
4.代码
//原有代码
class FileSplitter
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar;
public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_progressBar(progressBar){
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
}
}
};
class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
//扮演通知的角色,编译依赖,高层类依赖细节实现 违反依赖倒置原则
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, progressBar);
splitter.split();
}
};
//运用观察者模式后的代码
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
};
class FileSplitter
{
string m_filePath;
int m_fileNumber;
//IProgress* m_iprogress; //支持一个观察者
List m_iprogressList; // 抽象通知机制,支持多个观察者
public:
// 一个观察者的更新,多个观察一般采用添加和删除观察者的形式
// FileSplitter(const string& filePath, int fileNumber, IProgress* m_iprogress) :
// m_filePath(filePath),
// m_fileNumber(fileNumber),
// m_iprogress(iprogress){
// }
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber),{
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
onProgress(progressValue);//发送通知
}
}
//添加观察者和删除观察者
void addIProgress(IProgress* iprogress){
m_iprogressList.push_back(iprogress);
}
void removeIProgress(IProgress* iprogress){
m_iprogressList.remove(iprogress);
}
protected:
virtual void onProgress(float value){
List::iterator itor=m_iprogressList.begin();
List::iterator end=m_iprogressList.end();
for( ; itor != end; itor++ )
(*itor)->DoProgress(value); //更新进度条
}
}
};
//C++支持多继承,但一般不推荐多继承,一种特殊情况是一个基类,其它都是接口
class MainForm : public Form, public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
ConsoleNotifier cn;
FileSplitter splitter(filePath, number);
splitter.addIProgress(this); //订阅通知
splitter.addIProgress(&cn); //订阅通知
splitter.split();
splitter.removeIProgress(this);
}
virtual void DoProgress(float value){
progressBar->setValue(value);
}
};
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value){
cout << ".";
}
};
5.解析
这是一个文件分割程序。当文件过大时,将文件分割成若干份小文件分别读取,在这过程中需要通知用户分割完成进度。
在原有的代码中,FileSplitter是文件分割工具类,构造函数中包含文件路径、文件数目以及进度对象;FileSplitter:: split()函数用于实现文件分割和进度计算。但上述代码存在一个问题,即进度条表现形式ProgressBar是一个具体类,即容易根据实现需求发生变化,如进度条、数字等形式都能表现完成进度。而MainForm类编译时会依赖它,即高层类依赖细节实现,违反依赖倒置原则。
运用观察者模式后的代码中,IProgress是一个通知进度的抽象类,可将进度消息通知给需要知道的对象(例如屏幕等),具体怎么通知在其子类中实现。MainForm类继承IProgress,并订阅了消息,可在MainForm:: DoProgress()中具体实现进度展现形式;此外,还增加了另外一个通知对象cn,并在ConsoleNotifier:: DoProgress()定义了另一种具体实现进度展示的方法。该代码至少有两点优势:编译时MainForm依赖抽象类IProgress,不会违反依赖倒置原则;进度展示形式多样,可根据实际需求变化。
6.结构
Subject(目标,如FileSplitter):
目标知道它的观察者,可以有任意多个观察者观察同一个目标;
提供注册和删除观察者对象的接口。
Observer(观察者,如IProgress):为那些在目标发生改变时需要获得通知的对象定义一个更新接口;
ConcreteSubject(具体目标,如FileSplitter)
将有关状态存入各ConcreteObserver对象;
当它的状态发生改变时,向它的各个观察者发出通知。
ConcreteObserver(具体观察者,如MainForm,ConsoleNotifier)
维护一个指向ConcreteSubject对象的引用;
存储有关状态,这些状态应与目标状态保持一致;
实现Observer的更新接口使自身状态与目标状态保持一致。
7.总结
Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
观察者自己决定是否需要订阅通知,目标对象对此一无所知。
Observer模式是基于事件的UI框架中非常常用的设计模式,也是 MVC模式的一个重要组成部分。