将4年前的设计模式笔记再看一遍(3),Observer

一、模式总结

组件协作模式分类,通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。

依然是一个非常常用的模式。主要在讲,一个地方有了修改,其他地方如何接收这些修改。改进方向为符合依赖倒置原则------高层应用细节不应该影响底层设计。

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

1.原始版本

一个应用,需要将一个大文件进行拆分为多个小文件。拆分过程中,要显示进度。此示例,变化的地方在于,进度条的展现方式可能多种多样,可能需要在不同的界面展示进度。

如下代码,最开始的需求是,只需要在界面中的进度条中,将当前进度展示出来就好。

scss 复制代码
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){
            // ...
            if(m_progressBar != nullptr){
                float progress = m_fileNumber;
                progress = (i + 1) / progress;
                m_progressBar->setValue(progress);   
            }
        }
    }    
};

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.spilt();
    }
};

2、变更版本

现在有了需求变更,我们不是要在进度条中展现,而是同时在一个文本控件中将具体的进度值进行展示。那么上面代码中的两个类都需要改。可以有以下改进,让需求变更只需要在界面代码中改便好。

csharp 复制代码
class IProgress {
public:
    virtual void DoProgress(float value) = 0;
};

class FileSplitter {
    string m_filePath;
    int m_fileNumber;
    IProgress* m_iprogress;  // 重点在于此处, 解耦关键点
    
public:
    FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) :
        m_filePath(filePath),
        m_fileNumber(fileNumber),
        m_iprogress(iprogress){
        
    }
    
    void split() {
        // 1. 读取大文件
        
        // 2. 分批次向小文件中写入
        for(int i = 0; i< m_fileNumber; ++i){
            // ...
        
            float progress = m_fileNumber;
            progress = (i + 1) / progress;
            onProgress(progress);
        }
    }

protected:
    void onProgress(float value){
        if(m_iprogress != nullptr){
            m_iprogress->DoProgress(progress);
        }
    }
};

class MainForm : public Form, public IProgress {
    TextBox* txtFilePath;
    TextBox* txtFileNumber;
    ProgressBar* progressBar;
    // Label* progressLabel;
    
public:
    void Button1_Click(){
        string filePath = txtFilePath.getText();
        int number = atoi(txtFileNumber.getText().c_str());
        
        // 四年后再次看到这里,依然感觉此处很帅
        FileSplitter splitter(filePath, number, this);
        
        splitter.spilt();
    }
    
    virtual void DoProgress(float progress) {
        progressBar->setValue(progress);
        // progressLabel->setString(str(progress));   // c++么得str()
    }
};

3、更加完善的版本

上面的改进版本,已经符合依赖倒置原则。

其实还是有点问题的,对于上面的FileSplitter来说,只能通过构造函数为它添加一个Observer,当它的一个对象为全局的时候,在好几个界面中需要展示进度,则办不到了,不可能每次都为构造函数加参数。

以下改进,便可以支持多个观察者。

csharp 复制代码
class IProgress {
public:
    virtual void DoProgress(float value) = 0;
};

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 progress = m_fileNumber;
            progress = (i + 1) / progress;
            onProgress(progress);
        }
    }
    
    void addIProgress(IProgress* iprogress) {
        m_iprogressList.add(iprogress);
    }
    
    void removeIProgress(IProgress* iprogress) {
        m_iprogressList.remove(iprogress);
    }

protected:
    void onProgress(float value){
        List<IProgress*>::iterator iter = m_iprogressList.start();
        while(iter != m_iprogressList.end()){
            (*iter)->DoProgress(progress);
            iter++;
        }
    }
};

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());
        
        FileSplitter splitter(filePath, number);
        splitter.addIProgress(this);
        splitter.spilt();
    }
    
    virtual void DoProgress(float progress) {
        progressBar->setValue(progress);
    }
};

二、项目中的使用

最开始接触这种方式是在G15中,有一个全局模块作为Dispatcher,许多个模块(主要是UI界面)作为Listener。实现方式,就是在Dispatcher身上放了许多个Listener的相同参数函数,当Dispatcher身上某个数据改变的时候,就遍历这许多Listener放这边的函数,一一调用。

当时看到这一套消息监听的方式,觉得很高级,在看完这一篇教程后,发现这就是Observer(观察者)设计模式啊。

在现在项目中,也使用了这一套流程。Dispatcher提取出来一个基类,需要被许多Listener监听消息的模块,继承Dispatcher。Python中的实现与上面示例大同小异,唯一区别是提取出来了一套基类。

三、各种tip

1、软件设计中的细节,是极其容易变化的。底层设计不要依赖高层细节。

2、Java中的接口,在C++中,其实就是一个抽象基类。

3、多继承都是不推荐使用的。但是推荐一种继承方式,继承的父类为实类,其他的都只是接口。就是上面示例中的IProgress。

4、看这一系列视频对于理解C++,还真是有许多的帮助。

5、所写代码风格,和我所遵守的风格很是接近,这一套视频看起来真的很安逸。哈哈。

6、才第3个设计模式,发现比前面两个会难一点。不过依然在项目中有所使用。(然后四年后的我有将这一课完整再听一遍,老师讲的真好。)

7、上午和中午看视频,晚上自己进行文档整理的时候,还是有一些细节需要确认。再重新听一遍,竟然还是能够听进去。而且对讲课内容的理解,有了些变化。(这份笔记,四年后的我并没有很多改动。)

相关推荐
捕鲸叉4 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
wrx繁星点点4 小时前
享元模式:高效管理共享对象的设计模式
java·开发语言·spring·设计模式·maven·intellij-idea·享元模式
凉辰4 小时前
设计模式 策略模式 场景Vue (技术提升)
vue.js·设计模式·策略模式
菜菜-plus4 小时前
java设计模式之策略模式
java·设计模式·策略模式
暗黑起源喵4 小时前
设计模式-迭代器
设计模式
lexusv8ls600h6 小时前
微服务设计模式 - 网关路由模式(Gateway Routing Pattern)
spring boot·微服务·设计模式
sniper_fandc8 小时前
抽象工厂模式
java·设计模式·抽象工厂模式
无敌岩雀11 小时前
C++设计模式结构型模式———外观模式
c++·设计模式·外观模式
hxj..12 小时前
【设计模式】观察者模式
java·观察者模式·设计模式
XYX的Blog14 小时前
设计模式09-行为型模式2(状态模式/策略模式/Java)
设计模式·状态模式·策略模式