Rust·设计模式·观察者模式

 

观察者模式也可以成为订阅者模式 如何实现状态变化后,所有的依赖对象(订阅者)都能得到更新

  • 观察者模式也可以成为订阅者模式
  • 如何实现状态变化后,所有的依赖对象(订阅者)都能得到更新

缘起 Motivation

  • 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”
    • 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知
    • 如果这样的依赖关系过于紧密, 将使软件不能很好地抵御变化
  • 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系
    • 实现软件体系结构的松耦合

文件分割器

初级设计

界面

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();

  }
};

分割器

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)
        m_progressBar->setValue((float)(i+1)/m_fileNumber);
    }
  }
};

问题(8大设计原则)

  • 依赖倒置
    • 高层不依赖底层
      • 高层FileSplitter依赖了底层实现细节prograssBar
      • 进度展示方式可以有进度条、时钟、百分比、###等多种方式展现
      • 实现细节就是有需求可以扩展的点
      • 应对:
        • 单纯找prograssBar的基类ControlBase控件(可能是MFC就有的控件吧),但这个基类可能没有setVal这个方法
        • 再抽象一点,progressBar本质是一个通知机制
    • 抽象不依赖细节

依赖(编译时依赖):A依赖B,A需要编译时,B必须存在才能编译通过

重构设计

通知机制

class IProgress{
public:
  virtual void DoProgress(float value)=0;
  virtual ~IProgress(){}
};
  • I: Interface接口
  • 我们希望通过IProgress来表达抽象的通知机制
  • 之前的progressBar是一个通知控件

新的文件分割器

  • 把原来progressBar控件换成IProgress通知机制
  • 到此FileSplitter没有耦合任何界面类
    • 可以独立编译
class FileSplitter
{
  string m_filePath;
  int m_fileNumber;
  // 抽象通知机制,支持多个观察者
  List<IProgress*>  m_iprogressList; 
  
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++){
      //...
      if (m_iprogress != nullprt) {
        m_iprogress.DoProgress((i+1)/m_filenumber); // 更新通知
      }
    }
  }

主函数mainForm

  • MainForm要继承通知机制IProgress
  • Cpp只推荐一种多继承
    • 一个主继承类
    • 其他都是接口
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, this);

    splitter.split();
  }

  virtual void DoProgress(float value){
    progressBar->setValue(value);
  }
}

进一步优化

split() {
  for (int i = 0; i < m_fileNumber; i++){
    if (m_iprogress != nullptr) {
        m_iprogress.DoProgress((i+1)/m_filenumber); // 更新通知
    }
  }
}

优化为:

split() {
  for (int i = 0; i < m_fileNumber; i++){
      onProgress(float(i+1)/m_fileNumber); //发送通知
    }
  }
}

protected:
  virtual void onProgress(float value) {
    if (m_iprogress != nullptr) {
      m_iprogress->DoProgress(value); //更新进度条
    }
  }

如果要支持多个观察者

一个对象的状态改变,要通知所有的观察者

FileSpliter

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) {
  }

  // 增加、删除iprogress方法
  void addIProgress(IProgress *iprogress) {
    m_iprogressList.push_back(iprogress);
  }
  void removeIProgress(IProgress *iprogress) {
    m_iprogressList.remove(iprogress);
  }

  // 不变
  void split() {
    // ....
  }

  // 修改当前状态
protected:
  virtual void onProgress(float value) {
    List<IProgress *>::iterator itor = m_iprogressList.begin();

    while (itor != m_iprogressList.end())
      (*itor)->DoProgress(value); //随着进展,更新进度条
    itor++;
  }
}

主函数mainForm

主函数mainForm可以增加多个观察者(订阅者)

void Button1_Click(){

  string filePath = txtFilePath->getText();
  int number = atoi(txtFileNumber->getText().c_str());

  // 增加控制器观察者
  ConsoleNotifier cn;

  FileSplitter splitter(filePath, number);

  splitter.addIProgress(this); //订阅通知
  splitter.addIProgress(&cn) //订阅通知

  splitter.split();

  splitter.removeIProgress(this);
}

控制器订阅者

class ConsoleNotifier : public IProgress {
public:
  virtual void DoProgress(float value){
    cout << ".";
  }
};

模式定义

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

结构

  • 这个类图中Subject被单独提出来
  • 本代码中没有从ConcreteSubject中抽象提出Subject

要点总结

  • 使用面向对象的抽象
    • Observer模式使得我们可以独立地改变目标与观察者
      • 独立:松耦合
      • MainForm中可以随意添加订阅者
      • FileSplitter不需要修改都可以支持
    • 从而使二者之间的依赖关系达致松耦合
  • 目标发送通知时
    • 无需指定观察者
      • 由OnProgess()来发送通知
      • 不需要知道谁是订阅者
      • 而是针对整个订阅者列表
    • 通知(可以携带通知信息作为参数)会自动传播
      • 按照各个订阅者自己实现的方式传播(展示)订阅的内容
  • 订阅者自己决定是否需要订阅通知
    • 目标对象对此一无所知
  • Observer模式是基于事件的UI框架中非常常用的设计模式
    • 也是 MVC模式的一个重要组成部分

Rust 实现

功能说明

  • 热水器
    • 持续加热水温
    • 水温变化时,通知各个观察者
  • 观察者:洗澡水温
    • 当水温小于30度时,显示:水温太冷, 不能洗澡
    • 当水温在[30, 50)时,显示:水温正好,可以洗澡
    • 当水温在[50, 70) 时,显示:水温太热,不能洗澡
  • 观察者:烧开水
    • 当水温小于100度时,显示:水未烧开
    • 当水温大于100度时,显示:水已烧开

初始设计

#![feature(non_ascii_idents)]

trait IObserver {
    fn update(&self, subject: Box<&dyn ISubject>);
}
trait ISubject {
    fn add_observer(&mut self, observer: Box<dyn IObserver>);
    fn remove_observer(&mut self, observer: Box<dyn IObserver>);
    fn notify(&self);
    fn get_state(&self) -> u32;
    fn set_state(&mut self, state: u32);
}

struct 热水器 {
    tmpr: u32,
    observers: Vec<Box<dyn IObserver>>,
}
impl 热水器 {
    fn new() -> Self {
        Self {
            observers: Vec::new(),
            tmpr: 0,
        }
    }
}
impl ISubject for 热水器 {
    fn add_observer(&mut self, observer: Box<dyn IObserver>) {
        self.observers.push(observer);
    }
    fn remove_observer(&mut self, _observer: Box<dyn IObserver>) {
        unimplemented!()
        /*
        由于Rust不支持抽象类型的向下具体化,所以这个思路无法实现
        if let Some(idx) = self.observers.iter().position(|&ob| ob == observer) {
            self.observers.remove(idx);
        }
        */
    }
    fn notify(&self) {
        for observer in self.observers.iter() {
            observer.update(Box::new(self));
        }
    }
    fn set_state(&mut self, tmpr: u32) {
        self.tmpr = tmpr;
        println!("\n[热水器] 温度: {}", self.tmpr);
        self.notify();
    }
    fn get_state(&self) -> u32 {
        self.tmpr
    }
}

#[derive(PartialEq)]
struct 洗澡;
impl IObserver for 洗澡 {
    fn update(&self, subject: Box<&dyn ISubject>) {
        let tmpr = subject.get_state();
        if tmpr < 30 {
            println!("[洗澡] 水温太冷,不能洗澡");
        } else if 30 <= tmpr && tmpr < 50 {
            println!("[洗澡] 水温正常,可以洗澡");
        } else if 50 <= tmpr && tmpr < 70 {
            println!("[洗澡] 水温太热,不能洗澡");
        }
    }
}
#[derive(PartialEq)]
struct 开水壶;
impl IObserver for 开水壶 {
    fn update(&self, subject: Box<&dyn ISubject>) {
        let tmpr = subject.get_state();
        if tmpr >= 100 {
            println!("[开水壶] 水已烧开");
        } else {
            println!("[开水壶] 水未烧开");
        }
    }
}

fn main() {
    let mut 热水器 = 热水器::new();
    热水器.add_observer(Box::new(开水壶));
    热水器.add_observer(Box::new(洗澡));
    for tmpr in 0..12 {
        热水器.set_state(tmpr * 10);
    }
}