文章目录
观察者模式 - Observer
很多情况下,我们不仅仅关系实例的具体属性值,还关心实例的属性值变化情况,例如每日股票变化、每月净收入增减、每年年龄变化……
观察者模式是一种普遍且重要的设计模式,在诸多语言(尤其是面向对象语言)中都有它身影,遗憾的是C++没有提供可用的观察者模式实现
观察者 && 被观察者
假设我们正在检测一个实例属性 Person::age
是否发生变化,并将其显示出来
一个很容易想到的方式是,在 Person::set_age()
方法中检测 age
是否发生变化,同时将 Person::age
设为私有变量,避免发生监测范围外的修改
struct Person {
void set_age(const int value) {
if (value == age) return;
age = value;
cout << "age is changing" << endl;
}
private:
int age;
}
Observer 和 Observable 基本结构 及 其连接
然而其他类可能也面临同样的问题,因此我们更希望采用模板类 Observable
以应对多样化的需求,此外既然 Observable
是一个可被观测到类,那么它需要实现一些被观察者相应的功能:
- 维护一个观察者的列表,用于记录当前实例被哪些观察者观察
- 可以实现观察者的订阅和取消订阅,观察者订阅后将其存入观察者列表,取消订阅则删除
- 实现观察者和被观察者之间的信息传递
与之对应,观察者 Observer
需要支持获取来自被观察者的信息
template<class T>
struct Observer {
// do something
virtual void field_changed(T& source, const string& field_name) = 0;
};
template <class T>
struct Observable {
// call Observer
virtual void notify(T& source, const string& name);
// add new Observer
virtual void subscribe(Observer<T> *observer) { observers.push_back(observer); }
// delete Observer
virtual void unsubscribe(Observer<T> *observer);
private:
// Observers list
vector<Observer<T>*> observers;
};
template<class T>
void Observable<T>::unsubscribe(Observer<T> *observer) {
// erase-remove: erase multi-observer
observers.erase(
remove(observers.begin(), observers.end(), observer),
observers.end());
}
template<class T>
void Observable<T>::notify(T &source, const string &name) {
// call all observers
for (auto observer : observers) {
observer->field_changed(source, name);
}
}
erase-remove 是
STL
中一种非常实用的删除复数个元素的操作// remove作用为:移除(但不删除)[__first, __last)之间所有与__value相等的元素 // 原理为将不想等的元素依次前移,并且最后返回重排位置后的最后一个元素的下一位置 // 由于不删除元素,所以容器总大小并不会发生变化 template <class _ForwardIterator, class _Tp> _LIBCPP_NODISCARD_EXT _LIBCPP_CONSTEXPR_AFTER_CXX17 _ForwardIterator remove(_ForwardIterator __first, _ForwardIterator __last, const _Tp& __value); // vector<T>::erase 是删除[__first, __last)之间的元素 _LIBCPP_CONSTEXPR_AFTER_CXX17 iterator erase(const_iterator __first, const_iterator __last);
Person、Dog 以及 ConsoleObserver的实现
Person
和 Dog
作为被观察者Observable
,ConsoleObserver
作为观察者Observer
struct Person : Observable<Person> {
void set_age(const int value) {
if (value == age) return;
age = value;
notify(*this, "age");
}
int get_age() const {
return age;
}
private:
int age;
};
struct Dog : Observable<Dog> {
void set_name(const string& value) {
if (value == name) return;
name = value;
notify(*this, "name");
}
string get_name() const {
return name;
}
private:
string name;
};
struct ConsoleObserver : Observer<Person>, Observer<Dog> {
void field_changed(Person& source, const string& field_name) override {
cout << "Person's " << field_name << " has change to " << source.get_age() << ".\n";
}
void field_changed(Dog& source, const string& field_name) override {
cout << "Dog's " << field_name << " has change to " << source.get_name() << ".\n";
}
};
int main() {
Person person;
ConsoleObserver co1;
ConsoleObserver co2;
person.subscribe(&co1);
person.subscribe(&co2);
person.set_age(10); // Person's age has change to 10.
// Person's age has change to 10.
Dog dog;
dog.subscribe(&co1);
person.unsubscribe(&co2);
person.set_age(30); // Person's age has change to 30.
dog.set_name("pipi"); // Dog's name has change to pipi.
return 0;
}
通过信号(Signal)实现观察者模式
template <typename T>
struct Observable
{
virtual ~Observable() = default;
boost::signals2::signal<void(T&, const string&)> property_changed;
};
struct Person : Observable<Person>
{
explicit Person(int age): age(age){}
int get_age() const
{
return age;
}
void set_age(const int age)
{
if (this->age == age) return;
this->age = age;
property_changed(*this, "age");
}
private:
int age;
};
int main()
{
Person p{123};
auto conn = p.property_changed.connect([](Person&, const string& prop_name)
{
cout << prop_name << " has been changed" << endl;
});
p.set_age(20); // age has been changed
conn.disconnect();
getchar();
return 0;
}
线程安全
上述实现仅仅考虑了单线程,很容易发现, subscribe()
和 unsubscribe()
都会修改 vector
,一个很容易想到的解决办法是为所有涉及到 vector
的操作上锁,然而对操作加锁可能会导致可重入性问题,即我们不能从一个操作中调用另外一个操作,因为我们不能重复获得相同的锁
template <class T>
struct Observable {
// call Observer
virtual void notify(T& source, const string& name) {
scoped_lock<mutex> lock{latch_};
...
};
// add new Observer
virtual void subscribe(Observer<T> *observer) {
scoped_lock<mutex> lock{latch_};
...
};
// delete Observer
virtual void unsubscribe(Observer<T> *observer) {
scoped_lock<mutex> lock{latch_};
...
};
private:
// Observers list
vector<Observer<T>*> observers;
// lock
mutex latch_;
};
通过改变锁的作用范围,在读操作(notify()
)中避免可重入性问题很容易,只要我们通过锁获取一份当前vector
的副本vector_copy
,解锁后对vector_copy
进行任何读操作都不会带来可重入性问题
virtual void notify(T& source, const string& name) {
vector<Observer<T>*> observers_copy;
{
lock_guard<mutex> lock{latch_};
observers_copy = observers;
}
// do something
};
然而如何解决subscribe()
和 unsubscribe()
之间的资源冲突是一个问题
- 使用递归锁
recursive_mutex
,这允许一个线程多次获取锁,当然也需要相应次数的解锁锁才可以真正释放 - 使用
concurrent_vector
,尽管标准库并没有提供线程安全的容器,但是我们可以自己实现或者使用第三方提供的线程安全容器,这一切的前提是可以接受顺序先后不一致 - 业务实现避免重入问题
- 管理锁
- 每个线程维护自己的
thread_local
变量,当需要统计的时候,再将所有内容汇总,适用于订阅多,而取消订阅少的情况
参考资料
- 《Design Patterns in Modern C++》 Dmitri Nesteruk
- 《Effective C++ 改善程序与设计的55个具体做法》 Scott Meyers
- https://cplusplus.com