====C++设计模式 第一周====
本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\
/* Library & Framework Dev */
class Library {
public:
void Step1() { //... }
void Step3() { //... }
void Step5() { //... }
};
/* Application */
class Application {
public:
bool Step2(){ //... }
void Step4(){ //... }
};
/* Main workflow */
int main()
{
Library lib();
Application app();
lib.Step1();
if (app.Step2()) { lib.Step3(); }
for (int i = 0; i < 4; i++) {
app.Step4();
}
lib.Step5();
}
我们可以看到,因为整个程序的流程实现是交给应用层开发者的,因此流程是变化的,对于框架开发者来说也是不可控的。
\\
\\
而相比之下,如果将流程交由框架开发者来实现,也就是框架开发者在流程中反过来调用应用开发者创造的内容,我们称这种方法为**晚绑定**(//Late Binding//)。我们来看看代码是怎么实现的:
/* Libary & FrameWork with template method */
class Library {
public:
//Stable
void Run() {
Step1();
//variation -> virtual funtion call (step 2)
if (Step2()) { Step3(); }
for (int i = 0; i < 4; i++){
Step4(); //variation -> virtual funtion call (step 4)
}
Step5();
}
virtual ~Library(){ }
protected:
void Step1() {.....}
void Step3() {.....}
void Step5() {.....}
virtual bool Step2() = 0; //variation
virtual void Step4() = 0; //variation
};
/* Application */
class Application : public Library {
protected:
virtual bool Step2(){...}
virtual void Step4() {...}
};
int main()
{
Library* pLib=new Application();
lib->Run();
delete pLib;
}
}
框架开发者将 ''run()'' 函数,也就是整个程序的运行流程继承到了框架类中。由于框架开发者并不知道应用层开发者具体会如何设计第二步和第四步;因此将这两个方法定义成了虚函数;应用层开发者只需要在子类中通过多态重写这两个虚函数即可。相信你也看出好处在哪里了;应用层开发者只需要专注自己的开发内容即可,并不用去深入考虑整个流程,以及继承的内容是什么。而且,在将来维护的时候,只需要针对应用层的内容进行维护即可。
\\
\\
明白了这些,我们来看一看 //Template Method// 的具体定义吧:
\\
\\
>定义一个操作中的算法的骨架(**稳定**),而将一些步骤延迟(**变化**)到子类中。//Template Method// 使得子类可以在不改变(**复用**)一个算法的结构即可重新定义(//Override// 重写)该算法的某些特定步骤。
>——《设计模式》//GoF//
\\
一个稳定的框架,一个稳定的算法(工作流程),加上一些需求有变化的应用,通过 //Template Method// ,就可以让应用层的内容去复用这个算法(流程)了。前面三者是条件,最后一个是结果。这就是//Template Method// 的作用。
\\
\\
enum TaxBase {
CN_Tax,
US_Tax,
DE_Tax,
};
/* Tax Algorithm */
class SalesOrder {
TaxBase tax;
public:
double CalculateTax() {
if (tax == CN_Tax) { .... }
else if (tax == US_Tax) { .... }
else if (tax == DE_Tax) { .... }
}
};
这种方法看上去并没有什么太大的问题。但如果我们以是否具有拓展性的标准(也就是带着时间轴去看)去看待这种实现方法,我们会发现一些问题。
\\
\\
假设我们需要该系统增加对新的国家的税率计算支持,如果按照上面的实现方法,我们需要修改所有的类:
* 添加对应的判断条件到 ''TaxBase'' 中。
* 添加对应的税率算法到 ''SalesOrder'' 中。
只有完善了这些新的信息,我们的系统才能具有对新国家的税率的计算能力。然而,根据面向对象的设计原则,我们这里修改了好几个类(模块) 的功能。也就是说,我们没有在不修改原有模块的基础上进行拓展;在这一点上我们违反了**开放封闭原则**。
\\
\\
而从另一个角度上看,添加的这些算法,在一定程度上造成了对象的臃肿。原来的每个对象需要搭载 ''3'' 个算法,而添加新的国家以后,每个对象则需要搭载 ''4'' 个算法。在计算机中,我们每一次运算都要写入内存;如果代码较为精简,可能只需要写入一次CPU的高速缓存就能完成;但如果代码较为臃肿,则可能要分成好几次来写入,甚至会用上外部存储器空间。因此上述的设计,导致了因为空间的浪费而造成的效率低下;再者,如果对所有对象都执行 ''if-else'' 判断,会产生额外的计算负担。
\\
\\
因此,对于这样的情况,我们可以采用如下的设计方法,也就是 //Strategy// 模式。我们先来看一幅图,看看 //Strategy// 模式是如何对待这样的情况的:
\\
\\
/* Tax Algorithm base class */
class TaxStrategy {
public:
virtual double Calculate(const Context& context) = 0;
virtual ~TaxStrategy() {}
};
/* Tax Algorithm */
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) { .... }
};
/* if we wanna add a new tax calculate algorithm... */
class FRTax : public TaxStrategy {
public:
virtual double Calculate(const Context& context) { .... }
};
/* stable */
class SalesOrder {
private:
TaxStrategy* strategy;
public:
//we have the concrete Tax Type (Nation) here
SalesOrder(StrategyFactory* strategyFactory) {
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder() { delete this->strategy; }
public double CalculateTax() {
Context context();
//Calculate() will call the corresponding Algorithm
double val = strategy->Calculate(context);
}
};
\\
明白了以上的内容,再来看看 //Strategy// 模式的定义:
\\
>定义一系列算法,把它们一个个封装起来,并且使他们可以互相替换(变化)。该模式可以使算法可以独立于那些使用它的客户程序(稳定的)进行变化(扩展,子类化)。
>——《设计模式》GoF
也就是说,具体的算法是变化的,但抽象的算法是稳定的。如果要维持调用这些算法的客户程序的稳定,最好的办法就是用客户程序调用抽象的算法;然后使用抽象算法的具体实现来进行功能的拓展。
\\
\\
class FileSplitter
{
string m_filePath;
int m_fileNumber;
public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber),
void split(){
//...read file
//...split the file and write
for (int i = 0; i < m_fileNumber; i++) {
//...
}
}
};
class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
public:
void Button1_Click() {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number);
splitter.split();
}
};
现在我们需要添加一个显示进度条的功能。按照常理来说,我们应该直接去调用控件类;因为 ''MainForm'' 是我们的流程,因此,我们在 ''MainForm'' 的类中添加一个 ''ProgressBar'' 的对象(假设 ''ProgressBar'' 是我们要调用的具体控件类):
ProgressBar* m_progressBar;
因为具体的文件分割操作是由 ''FileSplitter'' 类实现的,因此文件的分割进度信息我们需要到该类里去获取。而获取该类内部信息的话,最容易想到的就是在 ''FileSplitter'' 类中再建立一个 ''ProgressBar'' 的对象,然后用这个对象通过一定的操作去获取当前分割的值。而查看 ''FileSpilitter'' 后,我们发现,具体分割的操作其实是由 ''FileSplitter'' 类的成员函数 ''split()'' 实现的;因此,我们可以在 ''split()'' 的分割循环中不停的更新分割的进度,然后交给 ''m_progressBar'':
for (int i = 0; i < m_fileNumber; ++i) {
//...
float progressValue = m_fileNumber;
//get current progress, handle it to m_progressBar
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
}
然后在 ''MainForm'' 中,对 ''FileSplitter'' 初始化,就可以源源不断的得到的进度信息了:
FileSplitter splitter(filePath, number, progressBar);
功能实现了;但仔细思考一下,以上的设计依赖关系是什么样的?
\\
\\
没错,在这个例子中,无论是主流程 ''MainForm'' ,还是功能类 ''FileSplitter'' ,都依赖了一个具体的控件类 ''ProgressBar'' 。 ''ProgressBar'' 是一个变化的类(进度条的实现可以是各种各样的形式);而在此处,功能类 ''FileSplitter'' 显然是稳定的;因此,我们的设计实际上依赖了一个变化的元素,这明显的违反了**依赖倒置原则**。
\\
\\
当然我们可以像以前一样去找 ''ProgressBar'' 的抽象父类;但问题又在于, ''ProgressBar'' 的抽象父类没有具体的方法可以供其对象调用。这就意味着我们不能使用该对象去接收当前的进度值。
\\
\\
如果考虑一下 ''ProgressBar'' 在系统里扮演了什么样的角色,我们会发现,它其实扮演了一个**通知**的角色。也就是说,比起使用一个具体的空间对象,我们可以使用一个抽象的通知来代替它的角色。
\\
\\
可以想到的是,不管进度条长啥样,它代表的始终是一个进度。因此,我们可以将进度条抽象为一个一定范围的数值;将这个数值交给不同的控件,就可以实现不同的进度条样式了:
class IProgress {
public:
virtual void DoProgress(float value) = 0;
virtual ~IProgress() {}
};
class FileSplitter {
//....
IProgress* m_iprogress;
//....
};
可以看到的是,'' IProgress '' 中有一个 ''DoProgress'' 的方法。我们可以通过在具体的实现流程中对这个方法进行重写,达到想要的进度条效果:
class MainForm : public Form, public IProgress {
//....
virtual void DoProgress(float value) {
progressBar->setValue(value);
}
};
\\
通过以上的改动,我们已经成功的实现了依赖导致原则:功能类 ''FileSplitter'' 再也没有依赖具体的控件类实现了;因此功能类 ''FileSplitter'' 实现了功能独立,可以用于任意的控件类的使用。具体的结果可以如下所示:
\\
\\
List m_iprogressList;
当然,为了实现对这些通知的管理,我们需要再设计一些函数:
void addIProgress(IProgress* iprogress) {
m_iprogressList.push_back(iprogress);
}
void removeIProgress(IProgress* iprogress) {
m_iprogressList.remove(iprogress);
}
通过上述的改写,我们实际上实现了如下图所示的结果:
\\
\\
class IProgress {
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress() {}
};
class FileSplitter {
string m_filePath;
int m_fileNumber;
List m_iprogressList; // 抽象通知机制,支持多个观察者
public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber) {
}
void split() {
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();
while (itor != m_iprogressList.end() )
(*itor)->DoProgress(value); //更新进度条
itor++;
}
}
};
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 << ".";
}
};
再来看看观察者模式的定义:
>定义对象间一种一对多(变化)的依赖关系,以便当一个对象(//Subject//)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
>——《设计模式》//GoF//
\\
\\
/* Base Class with operations */
class Stream {
public:
virtual char Read(int number) = 0;
virtual void Seek(int position) = 0;
virtual void Write(char data) = 0;
virtual ~Stream() {}
};
class FileStream:public Stream {
public:
virtual char Read(int number) { .... } // read FileStream
virtual void Seek(int position) { .... } // seek FileStream
virtual void Write(char data) { .... } // write FileStream
};
class NetworkStream :public Stream {
public:
virtual char Read(int number) { .... } // read NetworkStream
virtual void Seek(int position) { .... } // seek NetworkStream
virtual void Write(char data) { .... } // write NetworkStream
};
class MemoryStream :public Stream {
public:
virtual char Read(int number) { .... } // read MemoryStream
virtual void Seek(int position) { .... } // seek MemoryStream
virtual void Write(char data) { .... } // wr MemoryStream
};
那么此时的继承关系应该是这样的:
\\
\\
class CryptoFileStream :public FileStream{
public:
virtual char Read(int number){
//addtional crypto OP...
FileStream::Read(number); // read FileStream
}
virtual void Seek(int position){
//addtional crypto OP...
FileStream::Seek(position);// seek File Stream
//addtional crypto OP...
}
virtual void Write(byte data){
//addtional crypto OP...
FileStream::Write(data);//write File Stream
//addtional crypto OP...
}
};
class CryptoNetworkStream : :public NetworkStream{
public:
virtual char Read(int number){
//addtional crypto OP...
NetworkStream::Read(number);
}
virtual void Seek(int position){
//addtional crypto OP...
NetworkStream::Seek(position);
//addtional crypto OP...
}
virtual void Write(byte data){
//addtional crypto OP...
NetworkStream::Write(data);
//addtional crypto OP...
}
};
通过查看上面的代码,我们发现**基本操作下的**绝大部分的代码都是在做重复做同样的工作;也就是说,如果按照这样的结构来写程序,代码的冗余会非常高。找到了重复点,那么我们有没有办法来消除这些重复?
\\
\\
我们接着仔细观察,这些操作的不同在哪里?
FileStream::Seek(position);
NetworkStream::Seek(position);
可以看到的是,除了调用的类不一样,方法 ''seek()'' 是一模一样的;这说明了一个问题:对于拓展的功能使用继承来添加的话,是会有重复的,因为拓展的功能在严格意义上并不能代替基类;**只有在拓展的功能与主体类组合的情况下,才能代替基类**。
\\
\\
那么我们可以尝试着将继承的关系改为组合的关系试试,于是就有了:
class CryptoNetworkStream {
NetworkStream *stream; //switch inheritance to combination
.....
};
既然类中有了对应的主体类的对象,很显然我们可以通过该对象去调用对应的基本操作,于是就有了:
class CryptoNetworkStream {
NetworkStream *stream; //switch inheritance to combination
virtual void Seek(int position) {
stream -> seek(position);
}
.....
};
对比之前通过继承来实现的调用 ''NetworkStream::Seek(position)'',你会发现实际上面的代码做的是同一件事;但我们的子类 ''CryptoNetworkStream'' 就不用再苦哈哈的从主体类那里继承基础操作了,子类的体积是不是变小了许多?
\\
\\
我们接着将所有的加密子类都替换成上面的组合方式调用。当你替换完成的时候,你又会发现一件有趣的事情:
class CryptoFileStream {
FileStream *stream; //switch inheritance to combination
virtual void Seek(int position) {
stream -> seek(position);
};
class CryptoNetworkStream {
NetworkStream *stream; //switch inheritance to combination
virtual void Seek(int position) {
stream -> seek(position);
};
看到这里,我们突然明白了:上下的两个 ''seek()'',不就是多态的体现吗?而调用他们的对象,差别仅在于主体类的差异。那么这里我们完全可以在这里用多态处理这个对象;也就是将编译时的静态转化成了运行时的动态。于是,上面的程序可以改成:
class CryptoFileStream {
Stream *stream; //new Filestream
virtual void Seek(int position) {
stream -> seek(position);
};
class CryptoNetworkStream {
Stream *stream; //new NetworkStream
virtual void Seek(int position) {
stream -> seek(position);
};
以上的修改将所有的变化都丢到了运行时;也就是说,这几个看似不同的类,在编译阶段是完全等同的。他们唯一的不同,是在 Run time 决定的。因此,我们实际上就可以把上述的这种为了添加扩展功能而设计的众多子类合并到一起了。而因为我们这里有虚函数的存在,因此必须继承基类 //Stream// 来**指定接口规范**:
class CryptoStream : public Stream {
Stream *stream; //...
public:
virtual void Seek(int position) {
stream -> seek(position);
};
这就是装配模式的一大特点:既继承基类,又组合基类(这两个是完全不同的概念)。到此,我们成功的将加密操作做成了一个独立的模块;而这个模块在对之后主体类应用的过程中,呈现出了一种相加的状态:
FileStream* s1 = new Filestream(); //main class
CryptoStream* s2 = new CryptoStream(s1); // add option to main class
Stream* stream;
我们可以将这一段提到基类 //Stream//;但又出现了一个问题;我们的主体类并不需要这一段。为此,我们需要设计一个**中间类**(//Decorator//):
DecoratorStream: public Stream {
protected:
Stream* stream;//...
DecoratorStream(Stream * stm):stream(stm) {}
};
做好这个中间类以后,以后所有的拓展功能类,以及需要通过组合基类来实现操作的子类,都可以继承这个类了。而经过这么一段折腾,我们的类关系实际上变成了这样:
\\
\\
/* Base class */
class Messager {
public:
virtual void Login(string username, string password)=0;
virtual void SendMessage(string message)=0;
virtual void SendPicture(Image image)=0;
virtual void PlaySound()=0;
virtual void DrawShape()=0;
virtual void WriteText()=0;
virtual void Connect()=0;
virtual ~Messager(){}
};
/* platform Implementation */
class PCMessagerBase : public Messager{
public:
virtual void PlaySound() { .... }
virtual void DrawShape() { .... }
virtual void WriteText() { .... }
virtual void Connect(){ .... }
};
class MobileMessagerBase : public Messager{
public:
virtual void PlaySound() { .... }
virtual void DrawShape() { .... }
virtual void WriteText() { .... }
virtual void Connect(){ .... }
};
class PCMessagerLite : public PCMessagerBase {
public:
virtual void Login(string username, string password) { PCMessagerBase::Connect(); .... }
virtual void SendMessage(string message) { PCMessagerBase::WriteText(); .... }
virtual void SendPicture(Image image) { PCMessagerBase::DrawShape(); .... }
};
class PCMessagerPerfect : public PCMessagerBase {
public:
virtual void Login(string username, string password) { PCMessagerBase::Connect(); .... }
virtual void SendMessage(string message) { PCMessagerBase::WriteText(); .... }
virtual void SendPicture(Image image) { PCMessagerBase::DrawShape(); .... }
};
class MobileMessagerLite : public MobileMessagerBase {
public:
virtual void Login(string username, string password) { MobileMessagerBase ::Connect(); .... }
virtual void SendMessage(string message) { MobileMessagerBase ::WriteText(); .... }
virtual void SendPicture(Image image) { MobileMessagerBase ::DrawShape(); .... }
};
class MobileMessagerPerfect : public MobileMessagerBase {
public:
virtual void Login(string username, string password) {
MobileMessagerBase::PlaySound();
MobileMessagerBase::Connect();
}
virtual void SendMessage(string message) {
MobileMessagerBase::PlaySound();
MobileMessagerBase::WriteText();
}
virtual void SendPicture(Image image) {
MobileMessagerBase::PlaySound();
MobileMessagerBase::DrawShape();
}
};
可见的是,上面的代码有大量的重复。对于不同平台的不同版本在实现中,唯一不同的就是他们继承的类不同。按照我们在装配模式中学到的经验,这是可以通过继承转组合化简的,也就是把我们需要的不同版本(不同基础功能的组合)作为一个扩展功能来看待。因此,上面的一大堆子类也可以按照装配模式的方法来精简,于是可以化简成:
class MessagerLite {
MessagerImp* messagerImp;
public:
virtual void Login(string username, string password) { messagerImp->Connect(); .... }
virtual void SendMessage(string message){ messagerImp->WriteText(); .... }
virtual void SendPicture(Image image) { messagerImp->DrawShape(); .... }
};
class MessagerPerfect {
MessagerImp* messagerImp;
public:
virtual void Login(string username, string password) {
messagerImp->PlaySound();
messagerImp->Connect();
....
}
virtual void SendMessage(string message) {
messagerImp->PlaySound();
messagerImp->WriteText();
....
}
virtual void SendPicture(Image image){
messagerImp->PlaySound();
messagerImp->DrawShape();
....
}
};
到这里,添加不同功能组合的计划已经被实现了。不过回到先前的源代码中,我们发现有这么一段:
class PCMessagerBase : public Messager { ... }
仔细看一下,这里是有问题的。 ''Messager'' 是一个抽象类,里面全是**纯虚函数**;而 ''PCMessagerBase'' 中只重写了其中的一部分虚函数。按照虚函数的概念,纯虚函数是必须被重写的;因此我们得想一个办法让这里正确继承下去。这一个点,就是**桥模式**在解决问题上不同于装配模式的地方了。
\\
那要怎样组织类关系才合理呢?我们不妨来理顺一下思路:
- ''Lite'' 版本 和 ''Perfect'' 版本是通过 ''Messager'' 中的函数的前三个函数重写来实现的。
- 而上述这三个函数的实现,又包含了对后面四个函数的重写:比如 ''Login()'' 需要依赖 ''Connect()'' 来实现。
因此,我们明白一件事:''Lite'' 版本 和 ''Perfect'' 版本的实现是不区分平台的。只有当他们内部的功能函数需要具体化的时候,才会考虑平台的问题。也就是说,''Lite'' 版本 和 ''Perfect'' 版本的实现,实际上算是对 ''Messager'' 的一种重新诠释,而这个诠释的具体实现,是要交给不同的平台去应对的。
\\
\\
有了这个思路,我们可以做几件事来改进我们的程序了:
- ''Lite'' 版本 和 ''Perfect'' 版本的实现就是重写 ''Messager'' 中的前三个函数,其他的函数都是需要在不同平台中做具体实现的,因此可以把这些函数都独立出来,作为平台类的父类。
- 接下来,''Lite'' 版本 和 ''Perfect'' 版本的实现可以直接继承重写 ''Messager'' ,其内部实现就是通过 ''Messager'' 的对象调用前三个函数实现多态。
- 再者,我们分离出来的函数组成的类 ''MessagerImp'' 负责对先前三个函数的实现;因为有不同的平台要求,因此也通过继承多态到子类去做具体实现。
\\
做完这几步,我们发现我们将实现和抽象又一次的分开了,先前通过直接继承的复杂关系,到此也理清楚了。
\\
\\
总的来说,这就是一个抽象类会有自身的诠释,而诠释的过程需要交给具体的其他函数去实现。回头看看本节概述中关于桥模式所针对的问题的描述,也就明白了。在这里,变化的有两种:一种是抽象类的子类的变化;另外一种是子类中具体实现的函数的变化。把这两者分开后,两边的责任都非常明确。接着再以组合的方式拼到一起,我们就能得到一个清晰的程序结构,和更加简洁高效的代码。这就是桥模式所带给我们的。
\\
\\
来看看桥模式的定义:
>将抽象部分(业务功能)与实现部分(平台实现)分离,使他们都可以独立地变化。
>——《设计模式》//GoF//
\\
\\