Javascript设计模式——观察者模式与发布订阅模式的对比

1. 实现场景

分析的例子来源于:https://www.zhihu.com/question/23486749 作者:无邪气

存在有这样一个场景:当在一个数据中心中,用户需要从数据中心中取数据,等待数据中心将数据打包后,用户便可以获取数据。

2. 观察者模式的实现

2.1 UML类图

2.2 具体实现

在程序创建了一个任务中心后,再分别创建多个 DownloadTask 即创建多个下载任务,使用 dataHub.addDownloadTask() 来将下载任务添加到任务列表中,那么接下来当任务中心使用 dataHub.notify() 方法传入数据链接后,下载线程就会得到数据链接并实施具体的方法。

客户端不会去主动调用下载线程(观察者)的 finish() 方法,而是交给数据中心(被观察对象)去调用。

2.2.1 创建 DownloadTask 类作为观察者

DownloadTask类即为该系统中的观察者,观察者有 idloadedurl 属性,在其上面挂载了一个 finish() 方法,当被观察对象发出指令操作时,这个方法就会被触发。

1
2
3
4
5
6
7
8
9
10
11
function DownloadTask(id) {
this.id = id;
this.loaded = false;
this.url = null;
}

DownloadTask.prototype.finish = function(url) {
this.loaded = true;
this.url = url;
console.log('Task ' + this.id + ' load data from ' + url);
}

2.2.2 创建 DownloadTaskList 类作为管理器

DownloadTaskList类主要负责提供一个任务队列和一些附加的管理方法,方便管理观察者:

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
function DownloadTaskList() {
this.downloadTaskList = [];
}

DownloadTaskList.prototype.getCount = function() {
return this.downloadTaskList.length;
};

DownloadTaskList.prototype.get = function(index) {
return this.downloadTaskList[index];
};

DownloadTaskList.prototype.add = function(obj) {
return this.downloadTaskList.push(obj);
};

DownloadTaskList.prototype.remove = function(obj) {
const downloadTaskCount = this.downloadTasks.getCount();
while (i < downloadTaskCount) {
if (this.downloadTaskList[i] === obj) {
this.downloadTaskList.splice(i, 1);
break;
}
i++;
}
};

2.2.3 创建 DataHub 类作为被观察对象

DataHub类作为被观察对象,被观察对象通知观察者其实现原理就是在 DataHub 类的 notify() 方法中,去遍历数据中心中的下载队列(观察者队列)中的所有任务(观察者),在这些下载任务的实例(观察者)上调用其 finish() 方法,并传入参数 url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function DataHub() {
this.downloadTasks = new DownloadTaskList();
}

DataHub.prototype.addDownloadTask = function(downloadTask) {
this.downloadTasks.add(downloadTask);
};

DataHub.prototype.removeDownloadTask = function(downloadTask) {
this.downloadTasks.remove(downloadTask);
};

DataHub.prototype.notify = function(url) {
const downloadTaskCount = this.downloadTasks.getCount();
for (var i = 0; i < downloadTaskCount; i++) {
this.downloadTasks.get(i).finish(url);
}
};

2.2.4 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个数据中心
var dataHub = new DataHub();

// 现在用户来取数据了,创建两个任务
var downloadTask1 = new DownloadTask(1);
var downloadTask2 = new DownloadTask(2);

// 将任务添加到观察者队列中
dataHub.addDownloadTask(downloadTask1);
dataHub.addDownloadTask(downloadTask2);

// 数据打包完成了
dataHub.notify('http://somedomain.someaddress');

3.2.5 结果

1
2
Task 1 load data from http://somedomain.someaddress
Task 2 load data from http://somedomain.someaddress

3. 发布订阅模式的实现

3.1 UML类图

3.2 具体实现

3.2.1 定义 DataHub 类作为发布者

创建 DataHub 作为事件的发布者,当发布者调用 notify() 方法后,会触发一个回调函数,在这个回调函数中会去调用 DownloadManager 对象下的 publish() 方法,这样就相当于做了一个事件的发布。

1
2
3
4
5
function DataHub() {}

DataHub.prototype.notify = function(url, callback) {
callback(url);
}

3.2.2 定义 DownloadManager 类作为事件通道

DownloadManager 对象是发布订阅模式中的数据处理中心,它负责了事件的订阅与发布,包括处理发布的消息数据。

DownloadManager 类中有两个属性,一个是 events 存放了订阅事件以及对应事件的订阅者,uId 作为计数器,记录订阅者的ID。

其中,events 的结构为:

1
2
3
4
5
6
7
8
9
10
{ 
"订阅事件1": [
{taskId: Number, handler: Function}, // 订阅者1_1
{taskId: Number, handler: Function}, // 订阅者1_2
],
"订阅事件2": [
{taskId: Number, handler: Function}, // 订阅者2_1
{taskId: Number, handler: Function}, // 订阅者2_2
]}
}
1
2
3
4
function DownloadManager() {
this.events = {};
this.uId = -1;
}

在 DownloadManager 的追加一个 publish 方法函数,用来给发布者发布某一事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 发布
DownloadManager.prototype.publish = function(eventType, url) {
if (!this.events[eventType]) {
// 判断是否有订阅者订阅该事件,
return false;
}
//
var subscribers = this.events[eventType],
count = subscribers ? subscribers.length : 0;
// 循环遍历订阅事件队列中的订阅者
while (count--) {
var subscriber = subscribers[count];
subscriber.handler(eventType, subscriber.taskId, url);
}
}

在 DownloadManager 的追加一个 subscribe 方法函数,用来给订阅者订阅某一事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 订阅
DownloadManager.prototype.subscribe = function(eventType, handler) {
if (!this.events[eventType]) {
// 如果订阅的事件不存在,就在 events 对象中创建一个,让其值为一个空数组,用来存放订阅该事件的订阅者
this.events[eventType] = [];
}
var taskId = (++this.uId).toString();
// 将该订阅者放入对应的事件的订阅者队列中
this.events[eventType].push({
taskId: taskId,
handler: handler
});
return taskId;
}

3.2.3 客户端函数

客户端一定要遵循先设置订阅,后设置发布的原则。

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
// 创建一个数据中心
var dataHub = new DataHub();

// 创建一个下载事件管理器
var downloadManager = new DownloadManager();

// 创建一个下载器
var dataLoader = function(eventType, taskId, url) {
console.log('Task ' + taskId + ' load data from ' + url);
}

// 用户来请求数据了,从 downloadManager 订阅事件
var downloadTask1 = downloadManager.subscribe('dataReady', dataLoader);
var downloadTask2 = downloadManager.subscribe('dataReady2', dataLoader);

// 数据打包完成了
dataHub.notify('http://somedomain.someaddress', function(url){
// 向 downloadManager 发布一个事件
downloadManager.publish('dataReady', url);
});

dataHub.notify('http://somedomain2.someaddress', function(url){
// 向 downloadManager 发布一个事件
downloadManager.publish('dataReady2', url);
});

3.2.4 结果

1
2
Task 0 load data from http://somedomain.someaddress
Task 1 load data from http://somedomain2.someaddress

4. 区别

观察者模式不需要中间件,被观察对象可以直接将事件通知给观察者。

然而发布订阅模式,则需要一个中间的发布订阅管理器,来进行发布事件与订阅事件的详细方法实现。