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框架中非常常用的模式。
相关推荐
老歌老听老掉牙11 小时前
案例 7:二维 Rosenbrock 函数最小值(香蕉函数)
c++·pso
Severus_black12 小时前
【C++初阶】Stack、Queue算法题大练兵!
c语言·c++·算法·stl
杯子不吃紫菜15 小时前
LangGraph 体系实战(下):中断恢复、流式、子图与设计模式
单片机·嵌入式硬件·设计模式
xiaoye-duck16 小时前
《C++ 项目:手写大模型接入 SDK》DataManager数据管理模块:SQLite持久化存储
开发语言·数据库·c++·sqlite
是main不是漫17 小时前
【C++——类和对象(上)】筑基篇:以封装奠定面向对象之基
c语言·开发语言·c++
是2的10次方啊18 小时前
设计模式与生活⑳:楼里的事都找前台,你就再没见过师傅
设计模式
小七在进步1 天前
string的使用(1)
c++·visual studio
白杨尚青1 天前
C++进阶篇(六):map 和 set 的模拟实现——用红黑树封装 mymap/myset(迭代器 + operator[])
开发语言·c++·迭代器·stl·set·map·红黑树
kloc1 天前
聊天室项目入口网关子服务的实现
linux·c++
csdn_aspnet1 天前
C++ 3个有序点的方向(Orientation of 3 ordered points)
c++·算法