• [设计模式]观察者模式


    [设计模式]观察者模式

    1. 观察者模式解决什么问题

      观察者模式解决“通知依赖问题”,比如一个文件过滤器,你要显示进度,如何解决?我们最先想到的是里面生成一个通知方法,然后有关行为调用这个方法来进行通知。

      如下面一个代码,是一个文件分割器,将一个大文件分割成很多小文件;我们现在对其提出一个进度条需求,我们首先想到的就是如下在文件分割器中生成一个进度条对象,分割时不断更新进度条对象显示进度。

      可是,这存在什么问题呢?对于一般开发者,这显然并没有什么问题,但是如果对于面向对象,其违背原则如下:

      依赖倒置原则高层模块不能依赖底层模块,二者应该依赖于抽象;抽象不应该依赖于实现细节,实现细节应该依赖于抽象

       其 ProgressBar是一个实现细节,比如今天是以进度条展示的,但是你明天如果使用别的方式展示,你还是要更改源码。

       ① 现在,你想更改这段代码,你可以不依赖ProgressBar而是依赖其父类ControlBase,但你发现,其ControlBase并没有SetValue这个方法,你这就走入了一个死胡同。

       ② 现在,你往根本上思考,ProgressBar扮演了什么样的角色,其是一个“通知”的角色,可以是使用一个抽象的方式来表达通知而不是一个具体的控件来进行通知。

        因此,现在就不要使用一个具体的通知机制,而是使用一个通知抽象类 Progress,在构造函数中再传入其具体的子类实例化该基类,这样就完成了“从抽象到具体的转变”。

       ③ 之后,MainForm多继承自Progress,这样界面通知两者就构成了依赖关系,与文件分割器变成松耦合;而之前文件分割器中存在通知的具体实现细节。

        这样,以后这个界面可以放在任何环境下,都支持一个进度通知,可以被各种控件使用,而不一定非得是分割器使用,也不用靠分割器来实现其细节。

       ④ 到目前为止,已经很好解决了依赖倒置原则,但是目前仍然存在一个问题,如果要实现多个观察者,应该如何去实现?其采用vector设计多个观察者。

      总结:观察者模式也被称为订阅通知模式,即一个事件完成时可以选择通知到某个控件中,这种关系意味着其两者必须是松耦合而不是紧耦合,因为被观察者可以选择通知也可以选择不通知,让各个程序员根据需求来做出改变。

    // 文件分割器
    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);  // 进度条赋值
            }
    
        }
    };
    
    // 文件分割界面
    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();
    
        }
    };

     

    2. 修改代码

      根据上面的修改意见,我们将进度类作为一个虚类,其派生出一个进度条类,文件分割器构造函数中存在一个进度类,其初始化时作为进度条类的指针。

      其也可以实现进度条可选,即多个构造函数,在分割时会检查进度类指针是否为NULL,如果不为NULL,则调用通知。

      这明显,通过虚函数,原本进度条通知与文件分割紧耦合,现在变为松耦合,文件分割中没有具体的通知实现细节,其实现细节由专门的进度通知的派生类去实现。

      比如之后你想用标签去实现通知,则直接将进度类派发出一个然后调用即可。

    #include <stdio.h>
    #include <iostream>
    #include <windows.h>
    
    using namespace std;
    
    
    // 通知基类
    class IProgress {
    public:
        virtual void DoProgress(const int& current,const int& length) = 0; // 具体通知细节待定
        virtual ~IProgress() {} // 虚析构函数
    };
    
    // 实现进度条通知的类
    class ProgressBar :public IProgress {
    public:
        virtual void DoProgress(const int& current, const int& length) {
            putchar('[');
            for (int i = 0; i < length; i++) {
                putchar(i < current ? '>' : ' '); // 输出> 或者 ' '
            }
            putchar(']');
            printf("%3d%%", (int)((double(current) / length) * 100));
            // 光标回退,实现刷新
            for (int i = 0; i != length + 6; i++) {
                putchar('');
            }
        }
    };
    
    // 作为一个主界面
    class Window {
    
    };
    
    // 文件分割器类
    class FileSplit {
    private:
        IProgress* notifier; // 通知
        string fileName; // 文件名
        int splitNumber; // 文件分割次数
    public:
    
        // 分割器构造函数
        FileSplit(IProgress* notifier, string fileName, int splitNumber) :
            notifier(notifier), fileName(fileName), splitNumber(splitNumber) {}
    
        // 实现文件分割
        void DoFileSplit() {
            for (int i = 0; i < splitNumber; i++) {
                notifier->DoProgress(i, splitNumber);
                Sleep(1000); // 每次分割用时1s
            }
        }
    };
    
    // 界面类
    class MainForm : public Window{
    private:
        IProgress* iProgress; // 
    
    public:
        // 点击实现文件分割
        void Button_click() {
            iProgress = new ProgressBar(); // 选用进度条通知类型
            FileSplit* fileSplit = new FileSplit(iProgress, string("abc"), 20); // 生成文件分割器
            fileSplit->DoFileSplit(); // 开始分割文件
        }
    };
    
    int main() {
        MainForm* p = new MainForm;
        p->Button_click(); //点击分割文件 
    }

     3. 更进一步修改

      如果打算将其设计成一套桌面框架来使用,这里还存在一个问题:通知类与桌面类是松耦合,其关系不大。

      但是我们默认,一般桌面类必须存在一个通知类,只有这样才能实现让用户直观体现出其进程状态。

      从这个角度出发,我们将桌面类继承自通知类,因为通知类是一个虚函数,因此我们在桌面类中必须将其通知方法重写,这样就确保,如果生成一个桌面,就必须实现其通知类,实现桌面与通知的紧耦合。

    #include <stdio.h>
    #include <iostream>
    #include <windows.h>
    
    using namespace std;
    
    
    // 通知基类
    class IProgress {
    public:
        virtual void DoProgress(const int& current,const int& length) = 0; // 具体通知细节待定
        virtual ~IProgress() {} // 虚析构函数
    };
    
    // 作为一个主界面
    class Window {
    
    };
    
    // 文件分割器类
    class FileSplit {
    private:
        IProgress* notifier; // 通知
        string fileName; // 文件名
        int splitNumber; // 文件分割次数
    public:
    
        // 分割器构造函数
        FileSplit(IProgress* notifier, string fileName, int splitNumber) :
            notifier(notifier), fileName(fileName), splitNumber(splitNumber) {}
    
        // 实现文件分割
        void DoFileSplit() {
            for (int i = 0; i < splitNumber; i++) {
                notifier->DoProgress(i, splitNumber);
                Sleep(1000); // 每次分割用时1s
            }
        }
    };
    
    // 界面类
    class MainForm : public Window , public IProgress{
    
    public:
        // 点击实现文件分割
        void Button_click() {
            FileSplit* fileSplit = new FileSplit(this, string("abc"), 20); // 生成文件分割器
            fileSplit->DoFileSplit(); // 开始分割文件
        }
    
        // 实例化通知方式
        virtual void DoProgress(const int& current, const int& length) {
            putchar('[');
            for (int i = 0; i < length; i++) {
                putchar(i < current ? '>' : ' '); // 输出> 或者 ' '
            }
            putchar(']');
            printf("%3d%%", (int)((double(current) / length) * 100));
            // 光标回退,实现刷新
            for (int i = 0; i != length + 6; i++) {
                putchar('');
            }
        }
    };
    
    int main() {
        MainForm* p = new MainForm;
        p->Button_click(); //点击分割文件 
    }
  • 相关阅读:
    【Android Developers Training】 62. 搭建一个OpenGL ES环境
    【Android Developers Training】 61. 序言:使用OpenGL ES显示图像
    CentOS上安装mongodb
    Ubuntu 10.04上安装MongoDB
    linux awk 内置函数详细介绍(实例)
    无法import的原因(ImportError: No module named *****)
    C语言中volatile关键字的作用[转]
    数据对齐 posix_memalign 函数详解
    android ndk中使用gprof
    宽字符与窄字符
  • 原文地址:https://www.cnblogs.com/onetrainee/p/12730584.html
Copyright © 2020-2023  润新知