什么是观察者模式

观察者模式属于行为设计模式的一种,也就是说专注于改善或简化系统中不同对象之间的通信。

目的

定义对象间的一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新,以此达到解耦的目的。

别名

依赖(Dependents),发布-订阅(Publish-Subscribe),网上说的观察者模式和发布订阅模式不同其实是不对的。

有观察者对象(observer),就一定有观察者需要关注的目标对象(subject),观察者就好比订阅者,目标对象就好比发布者。

发布者:向订阅者们发送数据,一个目标可以有任意数目的依赖它的观察者,也就是一个发布者可能有许多订阅者
订阅者:数据到来时收到通知,消费数据,根据数据作出反应。总而言之,从发布者那里接受数据。

为了便于统一,下面用目标对象和观察者来做解释。

适用性

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用
  • 当对一个对象地改变需要同时改变其他对象,而不知道具体有多少对象需要改变。
  • 当一个对象必须通知其他对象,而又不能假定其他对象是谁。也就是说,不希望这些对象是紧密耦合的。

优缺点

优点:

  1. 广播通信,自动通知所有已经订阅过的对象。
  2. 目标对象和观察者之间抽象耦合

缺点:

  1. 创建可观察对象需要时间开销,可用惰性加载实现(把新的可观察对象的实例化推迟到需要发送事件通知的时候)
  2. 意外的更新(观察者不知道其他观察者的存在,可能对改变目标的代价一无所知)

实现

实现上先思考两个问题:谁主导(谁去维护登记,注销的动作),谁推送消息。
在谁主导这个问题上可以分为两类(好像废话):

  1. 目标对象主导,目标对象主动登记,推送,注销
1
2
3
4
5
6
7
8
9
10
//定义一个目标对象
var Publisher = new Observable;
//定义一个观察者
var Subscriber = function(news){};
//登记观察者
Publisher.subscribeCustomer(Subscriber);
//发布消息
Publisher.deliver('read all about it');
//注销观察者
Publisher.unSubscribeCustomer(Subscriber);
  1. 观察者主导,主动登记注销。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义两个目标对象
var NewYorkTimes = new Publisher;
var BeijingEveningNews = new Publisher;

//定义两个观察者
var Joe = function(from){
console.log('Delivery from '+ from + 'to Joe');
}
var Fy = function(from){
console.log('Delivery from '+ from + 'to Fy');
}
//观察者主动登记消息
//观察者拥有了登记和注销的权利
Joe.subscribe(NewYorkTimes);
Fy.subscribe(NewYorkTimes).subscribe(BeijingEveningNews);

//目标对象发送消息
NewYorkTimes.deliver('here is your paper');
BeijingEveningNews.deliver('hello');

在消息的传递上也分两种情况:

  1. 一个极端情况是,目标向观察者发送关于改变的详细信息,不管它们需要与否,我们称之为推模型(push model)。
  2. 另一个极端是拉模型(pull model),目标除最小通知外什么也不推送,全靠观察者显示地向目标询问细节。

拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。另一方面。拉模型可能效率较差,因为观察者对象需要在没有目标对象帮助的情况下确定什么改变了。

这也就是我们说的轮循机制还是通知回调机制,在js的具体地实现上,浏览器和node repl环境因为不知道谁登记过,所以采用了轮循机制,而自定义事件采用了通知回调机制。

应用场景

在DOM脚本编程环境中的高级事件模式中。事件监听器(listener)是一种内置的观察者,但是事件处理器(handler)不是。

  • 事件监听器:一个事件可以与几个监听器关联,每个监听器都能独立于其他监听器而改变。
1
2
3
4
5
6
7
8
9
var ele = $('ele');
var fn1 = function(e){
//handle click
};
var fn2 = function(e){
//do other stuff with click
};
addEvent(element,'click',fn1);
addEvent(element,'click',fn2);
  • 事件处理器:把事件传给与其关联的函数的手段,只能有一种回调方法
1
2
3
4
5
6
7
8
9
10
var ele = $('ele');
var fn1 = function(e){
//handle click
};
var fn2 = function(e){
//do other stuff with click
};
ele.onclick = fn1;
ele.onclick = fn2;
//fn2会覆盖fn1

演变与实践

根据观察者和目标对象主导地位的选择以及推拉模型的选择,观察者模式的实现也是多样化的,在面向对象的语言中通常都声明Subject(目标对象)和Observer(观察者)接口,再通过具体类去实现接口。

其实除了目标对象和观察者之外,设想当目标和观察者之间的依赖关系特别复杂时,这个时候可能就需要一个维护这些关系的更改管理器(ChangeMannager)对象,在java中可以通过消息队列去实现,在js中可以通过hash实现:

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
55
56
57
58
// 使用中间管理器,使用推模型
var pubsub = {};
(function(q){
var topics = {},
subUid = -1;
//发布或广播事件,包含特定的topic名称和参数(比如传递的数据)
q.publish = function(topic,args){
if(!topics[topic]){
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length:0;
while(len--){
subscribers[len].func(topic,args);
}
};

//通过特定的名称和回调函数订阅事件,topic/event触发时执行事件
q.subscribe = function(topic,func){
if(!topics[topic]){
topics[topic] = [];
}
var token = (++subUid).toString();
topics[topic].push({
token:token,
func:func
});
return token;
}
//基于订阅上的标记引用,通过特定topic取消订阅
q.unsubscribe = function(token){
for(var m in topics){
if(topics[m]){
for(var i=0,j=topics[m].length;i<j;i++){
if(topics[m][i].token === token){
topics[m].splice(i,1);
return token;
}
}
}
}
return this;
}
})(pubsub);

//test
var cb1 = function(topics,data){
console.log('第一个观察者:' + topics + ":" + data);
}
var cb2 = function(topics,data){
console.log('第二个观察者:' + topics + ":" + data);
}
var token1 = pubsub.subscribe('test',cb1);
var token2 = pubsub.subscribe('test',cb2);

pubsub.publish('test','第一次发布信息');
pubsub.unsubscribe(token2);
pubsub.publish('test','第二次发布信息');

推荐阅读

amplify publish 模块
radio.js
PubSubJS
bloody-jquery-plugins
jquery-tiny-pubsub
pubsubz
四人帮”Design Patterns”

写在最后,这里并没有详细的去叙述轮循轮询机制的消息队列和事件循环,一方面自己没有理清其中的关系,另一方面认识不是很深刻。日后再做总结。