C++设计模式-观察者模式


一、概念

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

观察者模式在现代编程中的重要性不仅仅在于它的实用性,更在于它所体现的思想——松耦合(Loose Coupling)。在这种设计中,对象间的相互作用不是通过紧密绑定的接口实现的,而是通过层次化、解耦的方式,增强了代码的灵活性和可维护性。

松耦合设计允许被观察者(Subject)维护一个观察者(Observer)列表,同时不需要了解这些观察者的具体实现。这种设计使得添加、移除或替换观察者变得简单,而不会影响被观察者或其他观察者的功能。这不仅提高了代码的可维护性,也增强了系统对未来变化的适应能力。

二、应用场景

  • 当一个对象的状态变化需要同时更新其他对象时。

三、定义方式

  • Observer(观察者):它是一个抽象类或接口,为所有的具体观察者定义一个更新接口,使得在得到主题的通知时更新自己。
  • Subject(主题):它维护了一系列依赖于它的Observer对象,并提供一个接口来允许Observer对象注册自己、注销自己以及通知它们。
  • ConcreteObserver(具体观察者):它实现了Observer接口,存储与Subject的状态自洽的状态。具体观察者根据需要实现Subject的更新接口,以使得自身状态与主题的状态保持一致。
  • ConcreteSubject(具体主题):它实现了Subject接口,将有关状态存入具体观察者对象,并在状态发生改变时向Observer发出通知。

四、实现方式

以下从按定义实现最基础的观察者模式功能,到实现实际应用场景的各种变形

4.1 基础方式

按定义实现最基础的观察者模式功能:

/**
 * @brief 定义一个Observer抽象基类
 */
class IObserver {
 public:
  IObserver() {}
  virtual ~IObserver() {}

 public:
  virtual void Update(int data) = 0;
};

/**
 * @brief 定义一个Subject抽象基类
 */
class ISubject {
 public:
  ISubject() {}
  virtual ~ISubject() {}

 public:
  virtual void Subscribe(std::shared_ptr<IObserver> observer) = 0;  // 观察者订阅事件
  virtual void Unsubscribe(std::shared_ptr<IObserver> observer) = 0;  // 观察者取消事件的订阅
  virtual void Notify(int data) = 0;  // 通知已订阅指定事件的观察者

 public:
  std::list<std::weak_ptr<IObserver>> observers_;  // 存放所有已订阅的observer
};

/**
 * @brief 实现一个具体的观察者
 */
class ConcreteObserver : public IObserver {
 public:
  ConcreteObserver(const std::string& name) : name_(name) {}
  virtual ~ConcreteObserver() override {}

 public:
  void Update(int data) override {
    std::cout << "observer [" << name_ << "] updated -> " << data << std::endl;
  }

 private:
  std::string name_;
};

/**
 * @brief 实现一个具体的Subject
 */
class ConcreteSubject : public ISubject {
 public:
  ConcreteSubject() {}
  virtual ~ConcreteSubject() override {}

 public:
  void Subscribe(std::shared_ptr<IObserver> observer) override {
    observers_.push_back(observer);
  };

  void Unsubscribe(std::shared_ptr<IObserver> observer) override {
    observers_.erase(std::remove_if(observers_.begin(), observers_.end(),
                                    [&observer](std::weak_ptr<IObserver> obj) {
                                      std::shared_ptr<IObserver> tmp =
                                          obj.lock();
                                      if (tmp != nullptr) {
                                        return tmp == observer;
                                      } else {
                                        return false;
                                      }
                                    }),
                     observers_.end());
  }

  void Notify(int data) override {
    for (auto it = observers_.begin(); it != observers_.end(); ++it) {
      std::shared_ptr<IObserver> ps = it->lock();
      // weak_ptr提升为shared_ptr
      // 判断对象是否还存活
      if (ps != nullptr) {
        ps->Update(data);
      } else {
        it = observers_.erase(it);
      }
    }
  }
};

// 测试
int main() {
  // 构造3个观察者对象
  std::shared_ptr<IObserver> observer1(new ConcreteObserver("observer1"));
  std::shared_ptr<IObserver> observer2(new ConcreteObserver("observer2"));
  std::shared_ptr<IObserver> observer3(new ConcreteObserver("observer3"));

  // 构造1个主题对象
  ConcreteSubject subject;

  // 为观察者订阅事件
  subject.Subscribe(observer1);
  subject.Subscribe(observer2);
  subject.Subscribe(observer3);
  // 通知订阅事件的观察者
  subject.Notify(10);

  // 模拟取消订阅
  subject.Unsubscribe(observer1);
  // 通知订阅事件的观察者
  subject.Notify(20);

  return 0;
}

控制台输出:

observer [observer1] updated -> 10
observer [observer2] updated -> 10
observer [observer3] updated -> 10
observer [observer2] updated -> 20
observer [observer3] updated -> 20

注意:在ISubject 中维护了一个已订阅的observer的list,在list中存放了observer对象的指针,在很多例子中list中直接存放observer的裸指针,在notify通知所有observer时,需要遍历list,调用每个observer的update接口,在多线程环境中,肯定不明确此时observer对象是否还存活,或是已经在其它线程中被析构了。本例这里使用了weak_ptr和shared_ptr替代observer裸指针,解决上述问题,同时还能避免循环引用

从上面的例子中可以看出:Observer是不依赖于Subject的,想要增加一个新的Observer只需要继承IObserver即可,无需修改Subject,这符合开闭原则,也实现了Observer与Subject的解耦。

4.2 改进观察者模式

上面例子中的观察者模式是经典模式,但是存在缺陷:

  • 需要继承,继承是强对象关系,只能对特定的观察者才有效,即必须是Observer抽象类的派生类才行;
  • 观察者被通知的接口参数不支持变化,导致观察者不能应付接口的变化

为了解决上例观察者模式的缺陷,可使用C++11 做出改进

  • 通过被通知接口参数化和std::function 来代替继承;
  • 通过可变参数模板和完美转发来消除接口变化产生的影响。

下面以一个具体的场景举例:

  1. 有一个数据中心,数据中心负责从其他地方获取数据,并对数据进行加工处理
  2. 有一个图表组件,需要从数据中心拿数据进行可视化展示
  3. 有一个文本组件,需要从数据中心拿数据进行可视化展示
  4. 后期可能还有更多的组件,需要从数据中心拿数据…
/**
 * @brief 实现一个具体的Subject,模拟一个数据中心
 */
template <typename Func>
class Subject {
 public:
  static Subject& GetInstance() {
    static Subject instance;
    return instance;
  }

 public:
  // 注册观察者,右值引用
  int Subscribe(Func&& f) { return Assign(f); }

  // 注册观察者,左值
  int Subscribe(const Func& f) { return Assign(f); }

  // 移除观察者
  void Unsubscribe(int id) { observers_map_.erase(id); }

  // 通知所有观察者
  template <typename... Args>
  void Notify(Args&&... args) {
    for (auto& it : observers_map_) {
      auto& func = it.second;
      func(std::forward<Args>(args)...);
    }
  }

 private:
  template <typename F>
  int Assign(F&& f) {
    int id = observer_id_++;
    observers_map_.emplace(id, std::forward<F>(f));
    return id;
  }

 private:
  Subject() = default;
  ~Subject() = default;
  Subject(const Subject&) = delete;
  Subject& operator=(const Subject&) = delete;
  Subject(Subject&&) = delete;
  Subject& operator=(Subject&&) = delete;

  int observer_id_ = 0;                //观察者对应编号
  std::map<int, Func> observers_map_;  // 观察者列表
};

/**
 * @brief 实现一个具体的观察者,模拟图表展示组件
 */
class ChartView {
 public:
  ChartView() {}
  ~ChartView() {}

 public:
  void Update(int data) {
    std::cout << "the chart data has been updated to [" << data << "]" << std::endl;
  }
};

/**
 * @brief 实现一个具体的观察者,模拟文本展示组件
 */
class TextView {
 public:
  TextView() {}
  ~TextView() {}

 public:
  void Update(std::string key, std::string value) {
    std::cout << "the text data has been updated to [" << key << ": " << value << "]" << std::endl;
  }
};

// 测试程序
int main(void) {
  ChartView cv;  // 图表展示组件
  // 从数据中心订阅
  Subject<std::function<void(int)>>::GetInstance().Subscribe(
      std::bind(&ChartView::Update, cv, std::placeholders::_1));

  TextView tv;  // 文本展示组件
  // 从数据中心订阅
  Subject<std::function<void(std::string, std::string)>>::GetInstance().Subscribe(std::bind(
      &TextView::Update, tv, std::placeholders::_1, std::placeholders::_2));

  // 这里模拟一个数据处理中心线程
  std::thread([]() {
    int cnt = 0;
    while (1) {
      // 模拟数据获取数据处理过程
      std::this_thread::sleep_for(std::chrono::seconds(1));
      auto time = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
      // 数据处理完成后,通知组件
      Subject<std::function<void(int)>>::GetInstance().Notify(cnt++);
      Subject<std::function<void(std::string, std::string)>>::GetInstance().Notify("time", std::to_string(time));
    }
  }).detach();

  // 主线程
  while (1) {
    std::cout << "main run ..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(5));
  }

  return 0;
}

控制台输出:

main run ...
the chart data has been updated to [0]
the text data has been updated to [time: 1723446799]
the chart data has been updated to [1]
the text data has been updated to [time: 1723446800]
the chart data has been updated to [2]
the text data has been updated to [time: 1723446801]
the chart data has been updated to [3]
the text data has been updated to [time: 1723446802]
main run ...
the chart data has been updated to [4]
the text data has been updated to [time: 1723446803]

在本例中,将Subject改为单例模式,这样在组件中调用注册接口,在数据中心调用通知接口,完全解耦分离;图表组件和文本组件所需的数据个数和数据类型是不同的,这在基础的观察者模式中是无法实现的,改进后的观察者模式脱离了需要继承的约束,可以实现更加通用的功能,后期扩展更多组件时,不需要修改Subject代码,只需要新增Observer即可。

总结

观察者模式的优点主要包括:
解耦:观察者和被观察的对象是抽象耦合的,即它们之间不直接调用,而是通过消息传递来通知。
灵活性:可以在运行时动态地添加或删除观察者。
复用性:观察者模式可以单独地重用主题和观察者。
缺点
开销:如果观察者非常多,那么更新的效率就会比较低,因为需要遍历所有的观察者,并调用它们的更新方法。

参考:
https://zhuanlan.zhihu.com/p/678950905
https://blog.csdn.net/QIANGWEIYUAN/article/details/88745835
https://download.csdn.net/blog/column/12397328/119132668

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐