属于类别

  1. 组件协作模式

动机

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

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

代码

文件分割器

需求:实现一个文件分割器

  1. //MainForm.cpp
  2. class MainForm : public Form{
  3. TextBox* txtFilePath; //文件路径
  4. TextBox* txtFileNumber; //分割的个数
  5. public:
  6. void Button1_Click() {
  7. string filePath = txtFilePath->getText();
  8. int number = atoi(txtFileNumber->getText().c_str());
  9. FileSplitter splitter(filePath, number);
  10. splitter.split();
  11. }
  12. }
  13. //FileSplitter.cpp
  14. class FileSplitter{
  15. string m_filePath;
  16. int m_fileNumber;
  17. public:
  18. FileSplitter(const string& filePath, int fileNumber) :
  19. m_filePath(filePath),
  20. m_fileNumber(fileNumber) {
  21. }
  22. void split() {
  23. //1. 读取大文件
  24. //2. 分批次向小文件中写入
  25. for(int i=0; i<m_fileNumber; ++i) {
  26. //...
  27. }
  28. }
  29. }

提供一个进度条

  1. //mainform.cpp
  2. class MainForm : public Form {
  3. TextBox* txtFilePath;
  4. TextBox* txtFileNumber;
  5. ProgressBar* progressBar;
  6. public:
  7. void Button1_Click(){
  8. string filePath = txtFilePath->getText();
  9. int number = atoi(txtFileNumber->getText().c_str());
  10. FileSplitter splitter(filePath, number, progressBar);
  11. splitter.split();
  12. }
  13. };
  14. //filesplitter.cpp
  15. class FileSplitter {
  16. string m_filePath;
  17. int m_fileNumber;
  18. ProgressBar* m_progressBar;
  19. public:
  20. FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
  21. m_filePath(filePath),
  22. m_fileNumber(fileNumber),
  23. m_progressBar(progressBar){
  24. }
  25. void split(){
  26. //1.读取大文件
  27. //2.分批次向小文件中写入
  28. for (int i = 0; i < m_fileNumber; i++){
  29. //...
  30. //进度条展示
  31. if (m_progressBar != nullptr) {
  32. float progressValue = m_fileNumber;
  33. progressValue = (i + 1) / progressValue;
  34. m_progressBar->setValue(progressValue);
  35. }
  36. }
  37. }
  38. };

抽象出基类

上面这样写有什么问题?

  • 不要想有什么设计模式, 而是想这样做的话会违反八大设计原则中的哪些条款
  • 违反了第一条设计原则,依赖倒置设计原则

依赖倒置原则:

  1. 高层模块不能依赖底层模块,二者都应该依赖于抽象
  2. 抽象不能依赖实现细节,实现细节应该依赖抽象

什么是依赖?

  1. A依赖B,A编译的时候,B需要存在,才能编译通过
  2. 我们在设计模式中所说的依赖,默认都是编译式依赖,除非明确提出是运行式依赖

那这个案例中产生了什么不良依赖?

  1. FileSplitter依赖于ProgressBar
  2. FileSplitter中的ProgressBar就是实现细节
    1. 目前展示进度用的是ProgressBar
    2. 但是不能确定明天使用的还是ProgressBar
    3. 有可能需求变了,明天改成了用label,直接以百分比的形式展现
    4. 也有可能这个程序明天要跨平台,是一个控制台程序,没有窗口。我希望在控制台上打一个一个点,来展示它的进度
  3. 所以当需求变更时,ProgressBar这个实现细节就会给我们带来困扰。为什么不喜欢依赖实现细节呢?因为实现细节非常容易变

那怎么样去解决这个问题,怎么样重构这个代码呢?
答:依赖倒置原则的解决方案。你不要去依赖A,而是去依赖A的抽象基类

  1. FileSplitter不要依赖ProgressBar,而是去依赖ProgressBar的基类
    1. ProgressBar的基类是什么呢?可能是ControlBase这样一个基类。但是单纯找ProgressBar的父类的话,你会发现你自己走入了一个死胡同。ControlBase可没有setValue的方法,甚至没有更新界面进度条的相关方法。
    2. 所以单纯找基类可能是一个很粗浅的方法。我们应该深入的思考,我们具体应该要解决什么问题。ProgressBar到底扮演了一个什么样的角色。
  2. ProgressBar其实扮演的是通知的角色,是一个通知控件。我们是不是可以用一个抽象的方式来表达一个通知,而不需要一个具体的控件来表达通知。

ProgressBar其实是一个通知控件,扮演了通知的角色。
我们可以把ProgressBar抽象成通知,然后让FileSplitter依赖通知抽象类,就能解决上一个问题。

  1. class IProgress{
  2. //用IProgress表达一种抽象的通知
  3. //而ProgressBar是一种具体通知控件
  4. public:
  5. virtual void DoProgress(float value) = 0;
  6. virtual ~IProgress(){}
  7. }
  8. //filesplitter.cpp
  9. class FileSplitter {
  10. string m_filePath;
  11. int m_fileNumber;
  12. //ProgressBar* m_progressBar;
  13. //ProgressBar的角色其实是“通知”
  14. //但我们不必用一个具体的控件(ProgressBar)来表示通知
  15. //可以用IProgress表达一种抽象的通知
  16. IProgress* m_iprogress; //抽象的通知
  17. public:
  18. FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) :
  19. m_filePath(filePath),
  20. m_fileNumber(fileNumber),
  21. m_iprogress(iprogress){
  22. }
  23. void split(){
  24. //1.读取大文件
  25. //2.分批次向小文件中写入
  26. for (int i = 0; i < m_fileNumber; i++){
  27. //...
  28. //进度条展示
  29. if (m_iprogress != nullptr) {
  30. float progressValue = m_fileNumber;
  31. progressValue = (i + 1) / progressValue;
  32. m_iprogress->DoProgress(progressValue);
  33. }
  34. }
  35. }
  36. };
  37. //mainform.cpp
  38. class MainForm : public Form, public IProgress {
  39. //C++虽然支持多继承,但不推荐使用多继承,它会带来很多很多比较复杂的耦合问题
  40. //但C++推荐一种多继承的形式:一个是主的继承类,其他都是接口(or抽象基类)
  41. //这也是后来的语言所提倡的(它们也是借鉴C++的经验):单继承是父类,其他是实现接口
  42. TextBox* txtFilePath;
  43. TextBox* txtFileNumber;
  44. ProgressBar* progressBar;
  45. public:
  46. void Button1_Click(){
  47. string filePath = txtFilePath->getText();
  48. int number = atoi(txtFileNumber->getText().c_str());
  49. FileSplitter splitter(filePath, number, this);
  50. splitter.split();
  51. }
  52. //实现IProgress接口
  53. virtual void DoProgress(float value) {
  54. progressBar->setValue(value);
  55. }
  56. };

如此,我们已经相当成功的将此案例满足了依赖倒置原则。把原来一个紧耦合变成了一个松耦合。

  1. FileSplitter已经不再依赖一个具体的界面类。所以,我们可以独立的编译FileSpliiter
  2. 界面类MainForm不存在之前,就可以编译FileSplitter。如此,将来我们就可以把FileSplitter放在具有窗口的界面,也可以放在命令行等等。

进一步优化:把FileSplitter中设置进度的代码抽成一个函数,甚至把它抽成虚函数,以供其子类重载

  1. class IProgress{
  2. public:
  3. virtual void DoProgress(float value) = 0;
  4. virtual ~IProgress(){}
  5. }
  6. //filesplitter.cpp
  7. class FileSplitter {
  8. string m_filePath;
  9. int m_fileNumber;
  10. IProgress* m_iprogress; //抽象的通知
  11. public:
  12. FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) :
  13. m_filePath(filePath),
  14. m_fileNumber(fileNumber),
  15. m_iprogress(iprogress){
  16. }
  17. void split(){
  18. //1.读取大文件
  19. //2.分批次向小文件中写入
  20. for (int i = 0; i < m_fileNumber; i++){
  21. //...
  22. //更新进度条
  23. float progressValue = m_fileNumber;
  24. progressValue = (i + 1) / progressValue;
  25. onProgress(progressValue);
  26. //1. 这样一改之后,发现代码更清楚了。
  27. }
  28. }
  29. protected:
  30. //2. 甚至有时候会写成虚函数,以便子类后续去改写
  31. virtual void onProgress(float value) {
  32. if (m_iprogress != nullptr) {
  33. m_iprogress->DoProgress(progressValue);
  34. }
  35. }
  36. };
  37. //mainform.cpp
  38. class MainForm : public Form, public IProgress {
  39. TextBox* txtFilePath;
  40. TextBox* txtFileNumber;
  41. ProgressBar* progressBar;
  42. public:
  43. void Button1_Click(){
  44. string filePath = txtFilePath->getText();
  45. int number = atoi(txtFileNumber->getText().c_str());
  46. FileSplitter splitter(filePath, number, this);
  47. splitter.split();
  48. }
  49. //实现IProgress接口
  50. virtual void DoProgress(float value) {
  51. progressBar->setValue(value);
  52. }
  53. };

支持多个观察者:观察者模式

还有一个问题,如果需要支持多个通知呢?

  1. 也就是需要多个观察者
  2. 我们现在只支持一个观察者MainForm(或者讲ProgressBar)

比如说这里多了一个类ConsoleNotifier,有一个问题:FileSpliiter只能接收一个观察者(FileSplitter构造函数中的第三个参数)

  1. //多填了一个观察者:控制台观察者
  2. class ConsoleNotifier : public IProgress{
  3. public:
  4. virtual void DoProgress(float value) {
  5. cout << value << endl;
  6. }
  7. }
  8. //mainform.cpp
  9. class MainForm : public Form, public IProgress {
  10. TextBox* txtFilePath;
  11. TextBox* txtFileNumber;
  12. ProgressBar* progressBar;
  13. public:
  14. void Button1_Click(){
  15. string filePath = txtFilePath->getText();
  16. int number = atoi(txtFileNumber->getText().c_str());
  17. //【问题】但是FileSpliiter只能接收一个观察者(第三个参数)
  18. FileSplitter splitter(filePath, number, this);
  19. splitter.split();
  20. }
  21. //实现IProgress接口
  22. virtual void DoProgress(float value) {
  23. progressBar->setValue(value);
  24. }
  25. };

优化:支持多个观察者

  1. class IProgress{
  2. public:
  3. virtual void DoProgress(float value) = 0;
  4. virtual ~IProgress(){}
  5. }
  6. //filesplitter.cpp
  7. class FileSplitter {
  8. string m_filePath;
  9. int m_fileNumber;
  10. List<IProgress*> m_iprogressList; //抽象的通知:支持多个观察者
  11. public:
  12. //要支持多个观察者,这里就不建议构造时传入了,建议使用add一次一次添加进来
  13. FileSplitter(const string& filePath, int fileNumber /*, IProgress* iprogress*/) :
  14. m_filePath(filePath),
  15. m_fileNumber(fileNumber)/*,
  16. m_iprogress(iprogress)*/ {
  17. }
  18. //add、remove表示我们支持多个观察者
  19. void addIProgress(IProgress* iprogress) {
  20. m_iprogressList.push_back(iprogress);
  21. }
  22. void removeIProgress(IProgress* iprogress) {
  23. m_iprogressList.remove(iprogress);
  24. }
  25. void split(){
  26. for (int i = 0; i < m_fileNumber; i++){
  27. //...
  28. float progressValue = m_fileNumber;
  29. progressValue = (i + 1) / progressValue;
  30. onProgress(progressValue);
  31. }
  32. }
  33. protected:
  34. virtual void onProgress(float value) {
  35. //通知每个观察者
  36. for(auto it=m_iprogressList.begin() it!=m_iprogressList.end(); ++it) {
  37. (*it)->DoProgress(progressValue);
  38. }
  39. }
  40. };
  41. //多填了一个观察者:控制台观察者
  42. class ConsoleNotifier : public IProgress{
  43. public:
  44. virtual void DoProgress(float value) {
  45. cout << value << endl;
  46. }
  47. }
  48. //mainform.cpp
  49. class MainForm : public Form, public IProgress {
  50. TextBox* txtFilePath;
  51. TextBox* txtFileNumber;
  52. ProgressBar* progressBar;
  53. public:
  54. void Button1_Click(){
  55. string filePath = txtFilePath->getText();
  56. int number = atoi(txtFileNumber->getText().c_str());
  57. ConsoleNotifier cn;
  58. FileSplitter splitter(filePath, number);
  59. //传入多个观察者
  60. splitter.addIProgress(this);
  61. spliiter.addIProgress(&cn);
  62. //注意:在这个过程中,内存管理要弄好
  63. splitter.split();
  64. //当然也可以移除观察者
  65. splitter.removeIProgress(this);
  66. }
  67. //实现IProgress接口
  68. virtual void DoProgress(float value) {
  69. progressBar->setValue(value);
  70. }
  71. };

到目前为止,就算真正的观察者模式。

模式定义

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

结构

红色部分是稳定的部分,是系统依赖的部分。蓝色部分是为了支持一对多的变化,也就是具体观察者的实现,具体主体对象的实现。
image.png

  1. Observer相当于IProgressObserver::Update()相当于IProgress::DoProgress
  2. ConcreteSubject相当于FileSplitter,简单来说就是主体对象,被监听的对象
  3. ConcreteObserver相对于MainForm或者ConsoleNotifiy。即具体的观察者。
  4. Subject是什么呢?

    1. 官方建议把Splitter抽象成Subject,并将FileSplitter的三个函数抽成基类的函数。即Subject::Attach()相当于FileSplitter::addSubject::Detach()相当于FileSpliiter::removeSubject::Notify()相当于FileSplitter:onProgress
    2. 当然,也有一些直接把Subject、ConcreteSubject合并成一个类,把Subject的三个函数直接写在ConcreteSubject内里面,比如我们所举的例子。SubjectConcreteSubject合起来相当于FileSplitter

      要点总结

      要点一:使用面向对象的抽象,Observer模式使得我们可以独立地改变目标观察者,从而使二者之间的依赖关系达致松耦合。
  5. 独立:表达的就是两者之间松耦合的关系,谁也不依赖谁,我改变不会影响到你,你改变也不会影响到我

  6. 对于目标(FileSplitter),添加多少个观察者,随你便(直接addIProgress就好)
  7. 对于观察者,适应多种多样观察者。是进度条,还是数字;是窗口,还是命令行程序都行。观察者怎么变,目标都不用变

要点二:目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播

  1. 发送通知时(FileSplitter::onProgress),FileSplitter不知道谁是观察者,通过抽象通知方法,将消息自动传播到具体的观察者

要点三:观察者自己决定是否需要订阅通知,目标对象对此一无所知

  1. MainForm::Button1_click()splitter.addIProgress中,观察者MainForm自己决定是否要订阅通知
  2. 目标对象(FileSplitter)对此一无所知

要点四:Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分

举例:

  1. C#中的Event事件模式就是观察者模式的一种表现

注意:观察者模式需要大家常用常思考,它可以在代码上有不同的展现形式。但是最关键的是:那个抽象的通知依赖关系

其他说法

【观察者】Observer
理念:将一个操作或一个系统分成两部分(观察者、实际运行的部分),把用户界面和业务逻辑进行分开处理。如显示器主机箱的关系,显示器只显示,主机负责所有处理与运算。
image.png
image.png
逻辑层面:

  1. Observer将一些控制信息反馈给用户
  2. Subject实现业务逻辑

物理层面:

  1. Observer一个或多个观察者类
  2. Subject主体类
  3. Container容器
  4. Register(this):一个观察者要得到主体类的数据的话,首先观察者需要提出申请(申请得到你的数据,申请作为一个前台,显示你的数据);之后,Subject会将观察者添加到一个Container中;主体数据有变化就立马告诉Container,容器内的所有成员就能得到主体变化的信息
  5. Unregister(this):注销观察

【例子】在一个公文处理系统中,开发者定义了一个公文类OfficeDoc,当公文的内容或状态发生变化时,关注此OfficeDoc类的相应DocExplorer对象,都更新其自身的状态。一个OfficeDoc对象能够关联一组DocExplorer对象,