设计模式-23种(二)

组件协同模式

现代软件专业分工之后的第一个结果是「框架与应用程序的划分」,「组件协作」模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时的常用模式。
下面是组件协同模式的三种典型模式。

Template Method(模板方法)

模板方法,就像高中老师讲的做题步骤一样,比如高考卷里的圆锥曲线题。

  1. 先设直线方程
  2. 把直线方程代入圆锥曲线
  3. 利用求根公式的定理,算出x1+x2,x1x2x_{1}+x_{2},x_{1}*x_{2}

  4. 很多时候,题目都可以用这样一个通用的模板方法来解决,只需换掉一丢丢的不同,大致步骤都是一样。

在代码编程中,模板方法就使用的相当多,通常,子类只需要重写父类给出的「可重写的方法」即可。

//base class
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; //变化

};

这个例子中base类给出了两个纯虚函数,我们可以override这两个函数,做一些符合自己的变化。

在调用的时候只需如下即可。

int main(){
	Library * app = new Application(); //Application 是Library的子类,并且重写了两个纯虚虚函数
	app->run();
}

熟悉安卓开发的同学,应该非常熟悉,很多时候,都是重写一个函数,例如生命周期的resume,stop函数等等,系统会自动调用,你重写之后的方法,是不是很神奇嘞,其中的奥秘就是这样捏。

总结

定义:「定义一个操作中的算法的骨架(稳定),而将一些步骤延迟(变化)到子类中。Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override重写)该算法的某些特定步骤」

下面是它的结构图。
image.png
模板方法是不是很好用,不要你调用我!让我来调用你。

Strategy(策略模式)

策略模式特别像模板方法。举个栗子,就能马上明白。

比如应用程序切换语言。
语言可能以后还会添加,可能有些人会这样写程序。

enum Language{
	ZH_CN,
	ENGLISH
};

int main(){
	Language cur_set_lang = get_cur_lang();
	if(cur_set_lang==ZH_CN){
		...//中文处理
	}else if(cur_set_lang==ENGLISH){
		...//英文处理
	}
}

试想一下,我们现在要增加另一门语言,比如台湾繁体。在上述代码里,就要在枚举类型中添加,同时还要在main函数中的if判断中增加新的处理。
当项目大起来时,直接修改,往往会令人抓狂。这十分的不优雅。

策略模式,就可以很好的解决这个问题。
可以看出来,每个if中都要对相应的语言做处理,我们可以给它抽象出来。

class Language{
public:
	virtual void process()=0;
	virtual ~Language(){}
};

天啊,看到这里,相比你已经知道了后续部分了,没错就像模板方法一样。

//中文类cpp文件
class Chinese:public Language{
	virtual void process()override{
		...//中文处理
	}
	virtual ~Chinese(){
		...//相关资源释放
	}
};

//英文类cpp文件
class English:public Language{
	virtual void process()override{
		...//英文处理
	}
	virtual ~English(){
		...//相关资源释放
	}
};

如此这般,添加新语言,只需要创建一个新的class类文件即可,避免了直接在源码上修改。
调用过程也变的简洁明了。

//值得一提的是,一般会与工厂方法一起使用,因为工厂方法还没介绍,先不要啦。
int main(){
	Language* cur_set_lang = get_cur_lang();
	cur_set_lang->process();
}

总结

定义:「定义一系列算法,把它们一个个封装起来,并且使他们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子变化)。」

结构如下
image.png
策略模式,可以很好的解决if else 这种没有止境或者不确定后续是否还会添加其他选项的代码,实在是妙啊。

Observe | Event(观察者模式)

观察者一般都是站在第三方角度上,observe中文有着「观察」的意思,而Event有着「事件」的意思,从字面意思上直观了解,不是那么容易懂。举个例子,当一个程序跑起来了,我们想看一下中间过程中发生的情况,这时我们就可以插入一段「代码(像Log)」可以让我们观察程序中间过程中的执行情况,也可以把这个观察,说成一种事件,「观察程序中间过程」事件。

比如一个分割文件的程序,我们想加一个进度条来显示实时进度,先来看一个简单的例子。

//main.cpp
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();

    }

};

在上面这段代码里,我们像FileSplitter类中传入了一个进度条实例来显示进度。
如果我们的FileSplitter类如下。

//FileSplitter.cpp
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);
        }
    }

};

这样写没毛病,让我们试想一下,如果此时我们再加一个进度条,比如在终端上打印进度信息,或者再加几个观察进度程序,如此上面的FileSplitter类将不能满足我们的要求。

『我们有时需要让一个类引起的变化,通知到其他一个或多个类。 』

继续之前的例子,按照我们的要求,当FileSplitter类变化时,其中我们加入的多个进度条类也会发生变化,也就是说FileSplitter类变化会通知其他进度条类。
image.png

这些需要FileSplitter类应该具有一些共同的函数等,可以把这些东西给抽象为一个接口。
例如这样

class IProgress{

public:

    virtual void DoProgress(float value)=0;

    virtual ~IProgress(){}

};

此时FileSplitter类就要这样修改了

class FileSplitter

{

    string m_filePath;

    int m_fileNumber;

    List<IProgress*>  m_iprogressList; // 抽象通知机制,支持多个观察者

public:

    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){

        auto itor=m_iprogressList.begin();
        while (itor != m_iprogressList.end() )

            (*itor)->DoProgress(value); //更新进度条

            itor++;
        }

    }

};

我们用一个vector容器存储需要通知的类,这些需要通知的类,需要继承IProgress接口,并且实现其中的纯虚函数,如此,我们就可以这样很方便的完成需求。
调用如下

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)override{

        progressBar->setValue(value);

    }

};

class ConsoleNotifier : public IProgress {

public:

    virtual void DoProgress(float value)override{

        cout << ".";

    }

};

总结

定义:「定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。」

结构如下
image.png

是不是有种学到啦的感觉,观察者模式,可以让一个类通知许多类的改变,仅仅把这些的共同地方抽象出来,就可以让 代码变的健壮起来。


单一职责模式

在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任

下面介绍两种经典的单一职责模式可以很快帮助我们理解。

Decorator(装饰器模式)

比如我们要做一个基于TCP的文件传输的小工具,IO方面,我们肯定需要处理文件流、网络流,甚至可能需要对这些流进行加密,或者对流进行缓存。

那么如果我们这些写,先抽象一个Stream的抽象类,包含read(),seek(),write() 三种方法。

class Stream{
publicvirtual char Read(int number)=0;
    virtual void Seek(int position)=0;
    virtual void Write(char data)=0;
    virtual ~Stream(){}
};

很容易就会想到文件流、网络流继承Stream

class FileStream{
publicvirtual char Read(int number){
		    ...
    }
    virtual void Seek(int position){
		    ...
    }
    virtual void Write(char data){
		    ...
    }
    virtual ~Stream(){}
};

class NetworkStream{
publicvirtual char Read(int number){
		    ...
    }
    virtual void Seek(int position){
		    ...
    }
    virtual void Write(char data){
		    ...
    }
    virtual ~Stream(){}
};

假设现在我们需要加密流和缓存流,新手一般会这样写。

class CryptoFileStream :public FileStream{

public:
    virtual char Read(int number){
        //额外的加密操作...
        FileStream::Read(number);//读文件流
    }

    virtual void Seek(int position){
        //额外的加密操作...
        FileStream::Seek(position);//定位文件流
        //额外的加密操作...
    }
    virtual void Write(byte data){
        //额外的加密操作...
        FileStream::Write(data);//写文件流
        //额外的加密操作...
    }
};

class CryptoNetworkStream : :public NetworkStream{
public:
    virtual char Read(int number){
        //额外的加密操作...
        NetworkStream::Read(number);//读网络流
    }

    virtual void Seek(int position){
        //额外的加密操作...
        NetworkStream::Seek(position);//定位网络流
        //额外的加密操作...
    }
    virtual void Write(byte data){
        //额外的加密操作...
        NetworkStream::Write(data);//写网络流
        //额外的加密操作...
    }
};

//-------------------
//缓存流
class BufferFileStream:public FileStream{
	...
};
class BufferNetworkStream:public FileStream{
	...
};

现在,回头看一下代码,很明显,每当我们去添加一个扩展功能类,比如反转流,这样就要再继承 FileStream、NetworkStream写两个类,或者当我们去添加一个流,比如内存流,这样就要继承内存流去写很多扩展类。

假设,FileStream、NetworkStream这样的类有N个,加密流这样的扩展有M个,那么一共要写1+N+NM1+N+N*M个。
20230427013929
不难发现,在扩展类中,实现的东西都相同,很容易联系到多态。那么扩展类就可以写成下面这样,比如CryptoStream类。

class CryptoStream :public Stream{
Stream* stream;
public:
	CryptoStream(Stream* s):stream(s){	
	}
    virtual char Read(int number){
        //额外的加密操作...
        stream->Read(number);//读文件流
    }

    virtual void Seek(int position){
        //额外的加密操作...
        stream->Seek(position);//定位文件流
        //额外的加密操作...
    }
    virtual void Write(byte data){
        //额外的加密操作...
        stream->Write(data);//写文件流
        //额外的加密操作...
    }
};

如此我们就可以省下很多重复的代码,只需要1+N+M1+N+M,非常nice。但是当我写出BufferStream类时,你会发现这其中又可以提出来共同的成员。

class BufferStream :public Stream{
Stream* stream;
public:
	BufferStream(Stream* s):stream(s){	
	}
	...
};

可以看到stream这个成员变量重复了,可以把这个给抽出来,单独写成一个类。

DecoratorStream: public Stream{
protected:
    Stream* stream;//...
    DecoratorStream(Stream * stm):stream(stm){

    }

};

这样在以后写扩展类时,继承这个DecoratorStream,既可以清楚意图,又减少了代码的重复。
例如BufferStream类就可以写成下面这样。

class BufferStream : public DecoratorStream{

public:

    BufferStream(Stream* stm):DecoratorStream(stm){

    }

    //...

};

调用程序如下

void Process(){
    //运行时装配
    FileStream* s1=new FileStream();
    CryptoStream* s2=new CryptoStream(s1);
    BufferStream* s3=new BufferedStream(s1);
    BufferStream* s4=new BufferedStream(s2);
}

总结

定义:动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)

结构如下
20230428012216
仔细想想,其实CryptoFileStream继承FileStream是不合理的,因为前者仅仅是后者的一个扩展,并没有is a的关系,也不朝着同一个方向变化。

Bridge(桥模式)

桥模式与装饰器模式十分相似,都是为了解决继承使得类数量爆炸增长的问题。

举个例子,现在要开发一个即时通讯软件,新手一般会先抽象一个类。
比如

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(){}

};

这很不错,PC和mobile端继承Messager,后续再继承pc类开发pc上的不同版本,例如lite,prefect版本等等。

class PCMessagerBase : public Messager{
public:
    virtual void PlaySound(){
        //**********
    }
    virtual void DrawShape(){
        //*********
    }
    virtual void WriteText(){
        //**********
    }
    virtual void Connect(){
        //**********
    }
};

class MobileMessagerBase : public Messager{

public:
    ...

};

接着写一下PC端的版本。

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::PlaySound();
        //********
        PCMessagerBase::Connect();
        //........
    }

    virtual void SendMessage(string message){
        PCMessagerBase::PlaySound();
        //********
        PCMessagerBase::WriteText();
        //........
    }

    virtual void SendPicture(Image image){
        PCMessagerBase::PlaySound();
        //********
        PCMessagerBase::DrawShape();
        //........
    }
};

这仅仅是PC端的,如果算上mobile端,又要加上两个业务类,加一个Linux平台,可能就要一个Linux base类,和两个业务类。
假如有N个平台,M个版本 ,那么总共就要写1+N+NM1+N+N*M个类,想想就十分可怕。

学习过上一节,很自然就会想「能不能使用多态来代替这个继承」。

首先我们先把Messager类分开,因为pc base类与mobile base类,仅仅实现了基础的部分,并没有实现login这些功能。

class Messager{
protected:
     MessagerImp* messagerImp;//...
public:
    virtual void Login(string username, string password)=0;
    virtual void SendMessage(string message)=0;
    virtual void SendPicture(Image image)=0;

    virtual ~Messager(){}

};

  
class MessagerImp{
public:
    virtual void PlaySound()=0;
    virtual void DrawShape()=0;
    virtual void WriteText()=0;
    virtual void Connect()=0;

    virtual MessagerImp(){}

};

下面只需让pc base类与mobile base类去实现MessagerImp接口,而业务类继承Messager类。

class PCMessagerImp : public MessagerImp{

public:
    virtual void PlaySound(){
        //**********
    }
    virtual void DrawShape(){
        //**********
    }
    virtual void WriteText(){
        //**********
    }
    virtual void Connect(){
        //**********
    }
};


class MobileMessagerImp : public MessagerImp{
public:
	...
};
class MessagerLite :public Messager {
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  :public Messager 
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();

        //........

    }

};

这样类的数量就缩短到了1+N+M1+N+M.
调用主程序,可以写成如下格式。

PCMessagerImp * pc = new PCMessagerImp();
Messager* mp = new MessagerPerfect(pc);
Messager* ml = new MessagerLite(pc);

总结

定义:将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。

结构如下
20230428224539
桥模式与装饰器模式,都有异曲同工之妙,都是将继承换为多态,总而避免了类数量的急剧增长。


对象创建模式

通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。

简单来说,就是让这个类文件中new的对象,像virtual函数那样,晚绑定。
也就是依赖倒置原则,不依赖与实现,应该依赖与抽象。

如果一个类文件中,有一处实现,那么可能在以后的过程中,就要去改这个类文件。