C++设计模式 #4 观察者模式(Observer/Event)

发布时间:2023年12月23日

观察者模式也是一种“组件协作”模式

动机

  • 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”。一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好的抵御变化。

使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

举个栗子

我们有一个将大文件分割成指定文件个数的任务。

class FileSplitter{
private:
    string m_filePath;
    int m_fileNumber;

public:
    FileSplitter(const string& filePath, int fileNumber) : 
        m_filePath(filePath), m_fileNumber(fileNumber){}

    void split(){
        //读取filePath文件

        //分批次向fileNumber个文件中写入
        for (int i = 0; i < m_fileNumber; i++){
            //写入文件
        }
    }
};
class MainForm : public Form{
private:
    TextBox* txtFilePath;
    TextBox* txtFileNumber;

public:
    void Button_Click(){
        string filePath = txtFilePath->getText();
        int number = atoi(txtFileNumber->getText().c_str());

        FileSplitter splitter(filePath, number);

        splitter.split();
    }
};

以上是实现这个基本文件分割需求的两个类的伪代码。分为实现文件分割的代码,和一个窗口按钮事件的代码。

现在我们需要在界面上显示其进度。这是一个经典的需要观察的场景,界面需要获取分割的实时进度,并且进行相应的处理。

class FileSplitter{
private:
    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(){
        //读取filePath文件

        //分批次向fileNumber个文件中写入
        for (int i = 0; i < m_fileNumber; i++){
            //写入文件
            
            if(m_progressBar!=nullptr)
                m_progressBar->setValue( (i+1) / m_fileNumber);  //更新进度条
        }
    }
};
class MainForm : public Form{
private:
    TextBox* txtFilePath;
    TextBox* txtFileNumber;

    ProgressBar* progressBar;

public:
    void Button_Click(){
        string filePath = txtFilePath->getText();
        int number = atoi(txtFileNumber->getText().c_str());

        FileSplitter splitter(filePath, number, progressBar);

        splitter.split();
    }
};

我们可能会想到在分割时获取分割进度,并且在界面中显示的方法。

存在的问题

这样的作法其实违背了依赖倒置的设计原则。

设计原则C++设计模式#1-CSDN博客

什么是依赖

我们这里讨论一下什么是依赖——如果A依赖B,那么A编译的时候需要B存在才能编译通过。也就是编译时依赖。

我们再看上面的代码实现,修改后的FileSplitter类依赖于组件类ProgressBar的实现。我们思考这样的情况,如果需求再次发生变更,从显示进度条修改成显示进度的百分比。那么我们就需要再次修改FileSplitter类的内容,换一个组件来实现这一功能。

这样的依赖是一个不好的依赖,因为实现的细节会经常发生改变。

观察者模式

重构代码

class IProgress{    //I表示这个类是一个接口,也就是表示这个类是一个抽象基类
public:
    virtual void DoProgress(float value) = 0;
    virtual ~IProgress(){}
};


class FileSplitter{
private:
    string m_filePath;
    int m_fileNumber;

    //ProgressBar* m_progressBar; 这是一个通知控件
    IProgress* m_iprogress; //抽象的通知机制

public:
    FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) : 
        m_filePath(filePath), m_fileNumber(fileNumber),m_iprogress(iprogressb){
        //...
    }

    void split(){
        //读取filePath文件

        //分批次向fileNumber个文件中写入
        for (int i = 0; i < m_fileNumber; i++){
            //写入文件
            
            if(m_iprogress!=nullptr)
                m_iprogress->DoProgress( (i+1) / m_fileNumber);  //更新进度条
        }
    }
};
class MainForm : public Form, public IProgress{
private:
    TextBox* txtFilePath;
    TextBox* txtFileNumber;

    ProgressBar* progressBar;

public:
    void Button_Click(){
        string filePath = txtFilePath->getText();
        int number = atoi(txtFileNumber->getText().c_str());

        FileSplitter splitter(filePath, number, this);

        splitter.split();
    }

    void DoProgress(float value){
        progressBar->setValue(value);
    }
};

如果我们使用单纯的使用找基类的方法来重构,我们可能发现,ProgressBar这个类的基类,很有可能根本没有setValue这个方法。

我们观察上面重构后的代码,我们将“通知”这一行为进行了抽象,不再使用通知组件这一个具体的对象来完成,而是通过一个抽象的虚的基类IProgress来实现。彻底将分割文件这个类与具体的组件类解耦。

我们让MainForm类进行一个多重继承,继承了IProgress的接口。再在其中进行DoProgress这一通知行为。(MainForm类是窗口的实现类,与具体的组件类紧耦合是正常的)

C++中尽量不要使用多重继承,但是推荐使用一种多重继承,就是一个是主继承类,其他都是接口。

进一步重构代码

目前我们实现了一个观察者的情况,如果需要有多个观察者的时候,需要进一步完善我们的机制。

class IProgress{    //I表示这个类是一个接口,也就是表示这个类是一个抽象基类
public:
    virtual void DoProgress(float value) = 0;
    virtual ~IProgress(){}
};


class FileSplitter{
private:
    string m_filePath;
    int m_fileNumber;


    vector<IProgress*> m_iprogressList; //多个观察者

public:
    FileSplitter(const string& filePath, int fileNumber) : 
        m_filePath(filePath), m_fileNumber(fileNumber){
        //...
    }

    void add_IProgress(IProgress* iprogress){
        m_iprogressList.push_back(iprogress);
    }

    virtual void remove_IProgress(IProgress* iprogress){
        m_iprogressList.remove(iprogress);
    }


    void split(){
        //读取filePath文件

        //分批次向fileNumber个文件中写入
        for (int i = 0; i < m_fileNumber; i++){
            //写入文件
            
            float progressValue = (i + 1) / m_fileNumber;
            onProgress(progressValue );
        }
    }

protected:
    void onProgress(float value){
        for(auto item = m_iprogressList.begin(); item != m_iprogressList.end(); item++){
            item->DoProgress(value);
        }
    }
};

可以将通知这一行为单独作为一个受保护的虚函数来实现,这也是现在一些框架的做法。

相应的MainForm类中,

FileSplitter splitter(filePath, number);

splitter.add_IProgress(this);
splitter.add_IProgress(.....);

模式定义

定义对象间的一种一对多(变化)的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并更新。 ——《设计模式》GoF

结构

类图中稳定的部分是红色的,蓝色部分是变化的。

对应上面的例子中的代码。

Oberver -> IProgress? ? ? ? Update() -> DoProgress()

ConcreteObserver -> MainForm/ProgressBar? ? ? ? Update() -> 重写的DoProgress()

Subject :?Attach()->addProgress()? ? ? ? Detach()->removeProgress()? ? ? ? Notify()->onProgress()这三个方法可以单独写成一个虚基类,也可以像上面代码中一样实现

ConcreteSubject ->?FileSplitter

总结

  • 使用面向对象的抽象,观察者模式可以独立地改变目标与观察者,达到松耦合
  • 目标发送通知时,无需指定观察者。通知可以携带信息自动传播。
  • 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
  • 观察者模式是基于事件的UI框架中非常常用的模式。
文章来源:https://blog.csdn.net/A11en3/article/details/135134175
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。