0%

观察者模式

Observer模式

本文参考现代C++设计模式-观察者模式章节

观察者模式是一种非常流行的、实用的设计模式。它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个对象,当这个对象的状态发生变化时,会通知所有观察者,使它们能够自动更新自己。

尽管观察者模式极为重要,但C++和标准库中都没有现成的实现。

下面将就一个安全的、正确实现的观察者模式的结构进行深入阐述。

以生日为例,当某人长大一岁时,我们可能要祝贺她的生日,但是要怎么做呢?

1
2
3
4
5
6
7
8
9
class Person{
public:
explicit Person(int age) : age{age} {};
void set_age(int value){
age = value;
};
int get_age(){
return age;
};

当年龄发生改变时,我们希望在set_age的地方对所有关心她的人发消息,然后做出一系列不同的操作,那么怎么连接关心她的人和她呢?这就是设计模式——观察者模式解决的问题:一对多的依赖关系

观察者

方法是定义某种基类,任何对她生日感兴趣的人都继承该基类。

1
2
3
4
5
template<typename T>class Observer{
virtual void field_changed(T& source,const string&field_name){
//doing_something();
}
};

上述代码是一个类模版的写法,Observer类有一个名为T的模版类型参数,用来表示Observer保存的元素类型,其中第一个参数是对其字段实际更改的对象的引用,第二个是字段的名称。如此,继承该模版类的实现将允许其他对象观察到该类的更改。

观察者类

1
2
3
4
5
class PersonObserver:Observer<Person>{
void field_changed(Person& source, const string&field_name) override{
cout<<"Person's " << field_name << " has changed to "<<source.get_age()<<endl;
}
};

继承Oberver类的类中方法可以被重写,进而用于观察多个类的属性变化。代码示例如下:

1
2
3
4
5
class PersonObserver:Observer<Person>,Observer<Creature>
{
void field_changed(Person& source, ...) { ... }
void field_changed(Creature& source, ...) { ... }
};

以上是对观察者类的定义,即观察别人的类的描写,那么被观察者应该做些什么呢?

被观察者

在此问题中应考虑的是Person类的职责。

  • 内部保存一个所有对Person类的改变感兴趣的观察者列表
  • 提供增加/删除订阅者的操作
  • 在字段发生改变时通知所有观察者

当然这些功能都可以很好的被移动到一个单独的基类中,以增加代码的复用性。

1
2
3
4
5
6
7
8
9
10
template <typename T> public class Observable{
void notify(T& source, const string& name) {
for(auto obs:observers)
obs->field_changed(source,"age");
}
void subscribe(Observer<T>* f) { observers.push_back(f); }
void unsubscribe(Observer<T>* f) { ... }
private:
vector<Observer<T>*> observers;
};

当然仅仅是继承该类还是不够的,我们还需要在字段更改时调用notify方法,Person类应被改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person:public Observable<Person>{
public:
explicit Person(int age):age(age){};
void set_age(int age){
if(this->age==age)return;
this->age=age;
notify(*this,"age");
}
int get_age(){
return age;
}

private:
int age;
};

现在我们就可以在这些基础上实现 观察者模式了。

源代码实现

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <vector>

using namespace std;

//观察者模版类,职责是当被观察者类发出消息时执行所对应的操作
template<typename T> class Observer{
public:
virtual void field_changed(T& source, const string&field_name) = 0;
};
//被观察者模版,职责是存放对被观察者感兴趣的观察者列表,维护列表的增删改查,通知观察者
template <typename T> class Observable{
public:
void notify(T& source, const string& name) {
for(auto obs:observers)
obs->field_changed(source,"age");
}
void subscribe(Observer<T>* f) { observers.push_back(f);}
void unsubscribe(Observer<T>* f) {}
private:
vector<Observer<T>*> observers;
};
//继承被观察者类模版的类,注意要想在外部使用模版类的函数,必须使用公有继承。在需要通知的地方调用通知函数
class Person:public Observable<Person>{
public:
explicit Person(int age):age(age){};
void set_age(int age){
if(this->age==age)return;
this->age=age;
notify(*this,"age");
}
int get_age(){
return age;
}
private:
int age;
};
//继承观察者类模版的类,同样注意公有继承。override的字段使得其改变了模版类中的函数格式。
class PersonObserver:public Observer<Person>{
void field_changed(Person& source, const string&field_name) override{
cout<<"Person's " << field_name << " has changed to "<<source.get_age()<<endl;
}
};
//主函数部分,构建了观察者与被观察者。
int main()
{
Person p(20);
PersonObserver cpo;
p.subscribe(&cpo);
p.set_age(21);
p.set_age(23);
return 0;
}

显示结果为:

1
2
Person's age has changed to 21
Person's age has changed to 23

如果不关心属性依赖关系、线程安全和可复用问题的话,上面的实现模式可以很好的被应用。由于某些原因,我很关心线程安全问题。

线程安全问题

上面的实现中忽略的问题是:观察者如何取消对某一被观察对象的订阅。这在单线程中十分简单。

1
2
3
4
5
6
void unsubscribe(Observer<T>* observer)
{
observers.erase(
remove(observers.begin(), observers.end(), observer),
observers.end());
};

虽然在技术上是正确的,但是在多线程中同时调用订阅函数和取消订阅函数会导致未定义行为出现,这可能出现意想不到的后果。

书中提到了两种解决方案:

第一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class Observalbe{
void notify(T&source,const string& name){
scoped_lock<mutex> lock{ mtx };
}
void subscribe(Observer<T>* f){
scoped_lock<mutex> lock{ mtx };
}
void unsubscribe(Observer<T>* o){
scoped_lock<mutex> lock{ mtx };
}
private:
vector<Observer<T>*> observers;
mutex mtx;
}

第二种:

实用TPL/PPL中concurrent_vector之类的东西,当然,这个时候你添加对象的顺序不是他们得到通知的顺序,好处是不用再管理锁了。

值得一提的是:如果需要被订阅的对象和订阅对象的关系是一个不变量的话,线程安全问题就不需要考虑了。

一些问题

上述源代码毫无疑问是实现了观察者模式、而且是使用类模版这种十分轻松的方式,但是我在实际问题的使用中遇到了一些困难。比如:

老师在考场监督学生这一情景。我的思路是构建老师类和学生类,分别继承观察者类模版和被观察者类模版。

然后在notify函数的具体使用时发现了问题,观察者和被观察者都是Person类型的。所以在notify的模版类定义中不需要考虑notify第一项的指针类型,因为观察者和被观察者都是同一类型的。但是学生与老师这个问题中,被观察者模版中使用的应该是学生类(可被学生类观察),而且notify的自我指针引用时应该是老师类,但在类模版声明是是学生类,所以编译器无法通过。

我想的办法是将被观察者模版类的类型参数变为两个,一个是被观察者自身,一个是观察者,这样就可以将两者分开。我添加之后是这样的情况:

avatar

avatar

这里我其实有点不理解,this指针是Teacher类之中的,为什么是Student类型的指针??

我对模版类的理解还是太浅了,所以想用模版类实现不同类型的观察者对同一类型的被观察者暂时放置一边,而采用抽象类接口的方式实现观察者模式。

抽象类继承实现

思路:

  • 将需要被观察的量抽象为被观察者抽象类,包含添加观察者、删除观察者、通知观察者和内部的观察者信息
  • 将观察者抽象为观察者类,具体为根据不同的对象,重载不同的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//观察者抽象类
class Observer{
public:
virtual void updata(const std::string) = 0;
};
//被观察者抽象类
class Observable{
public:
void notify(std::string name){
for(auto obs:observers){
obs->updata(name);
}
}
void addObserver(Observer* ob){
observers.push_back(ob);
}
private:
std::vector<Observer*> observers;
};

这样就构建了两个抽象类,分别是观察者和被观察者,它们的职责是很好理解的,那么下面我将实现不同类型的观察者对同一被观察者的同一个动作做出不同的回应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Teacher:public Observable{
public:
void exam_start(){
std::cout<<"exam start"<<std::endl;
notify("exam_start");
}
};

class Student:public Observer{
public:
void updata(std::string str){
if(str =="exam_start"){
std::cout<<name<<" start_write"<<std::endl;
}
}
std::string name;
explicit Student(std::string name):name(name){};
};

class bigTeacher:public Observer{
public:
void updata(std::string str){
if(str =="exam_start"){
std::cout<<name<<" I know"<<std::endl;
}
}
std::string name;
explicit bigTeacher(std::string name):name(name){};
};

int main()
{
Teacher a;
Student b = Student("zhang");
Student c = Student("Li");
bigTeacher d = bigTeacher("hah");
a.addObserver(&b);
a.addObserver(&c);
a.addObserver(&d);
a.exam_start();
return 0;
}

上面我实现了两个不同的观察者,学生和巡考老师,他们监听考试开始的信号,并作出回应。

以上的这种方式可以看成某个对象的自身状态的改变而引起其他对象做出相应的回应这样一种写法。这个被观察者可以是某个实体对象的某个属性,也可以是某个动作,某个事件。

观察者模式应用场景

当某一方的行为依赖另一方的行为或者状态时,可以使用观察者模式松耦合联动双方,使得一方的变动可以通知到感兴趣的另一方对象,从而让另一方对象做出回应。

适用情形:

  1. 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  2. 当一个模型有两个方面,其中一个依赖另一个时,可将两者封装在独立的对象中使它们可以各自独立的改变和复用。
  3. 实现类似广播机制的功能,不需要知道具体收听者,只需要发广播
  4. 多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域通知(超过两种观察者)