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

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

动机

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

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

举个栗子

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

cpp 复制代码
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++){
            //写入文件
        }
    }
};
cpp 复制代码
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();
    }
};

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

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

cpp 复制代码
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);  //更新进度条
        }
    }
};
cpp 复制代码
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类的内容,换一个组件来实现这一功能。

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

观察者模式

重构代码

cpp 复制代码
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);  //更新进度条
        }
    }
};
cpp 复制代码
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++中尽量不要使用多重继承,但是推荐使用一种多重继承,就是一个是主继承类,其他都是接口。

进一步重构代码

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

cpp 复制代码
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类中,

cpp 复制代码
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框架中非常常用的模式。
相关推荐
左手の明天3 小时前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
青少儿编程课堂3 小时前
后缀自动机解析:本质不同子串与最长重复片段统计
c++·python·算法·bfs·信息学竞赛
朝朝辞暮i3 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
无名猿5 小时前
shared_ptr 完全指南:引用计数、控制块与开销
c++·性能优化·内存管理·标准库·现代c++
geovindu7 小时前
rust: Composite Pattern
开发语言·后端·设计模式·rust·组合模式
xailorliu7 小时前
SLIC_超像素算法
c++·opencv·目标检测·机器学习·计算机视觉
苏打豆8 小时前
CRISP源码阅读——基于ROS2力矩反馈控制器与速度加速度零空间
c++·线性代数·算法·矩阵·机器人
小小龙学IT9 小时前
snap7 开源西门子 S7 通信协议库深度解析
c++·开源
小溪学编程9 小时前
从 C++ 的规范变迁看语言发展
jvm·c++·面试