1. 实现场景
分析的例子来源于:https://www.zhihu.com/question/23486749 作者:无邪气
存在有这样一个场景:当在一个数据中心中,用户需要从数据中心中取数据,等待数据中心将数据打包后,用户便可以获取数据。
2. 观察者模式的实现
2.1 UML类图
2.2 具体实现
在程序创建了一个任务中心后,再分别创建多个 DownloadTask 即创建多个下载任务,使用 dataHub.addDownloadTask()
来将下载任务添加到任务列表中,那么接下来当任务中心使用 dataHub.notify()
方法传入数据链接后,下载线程就会得到数据链接并实施具体的方法。
客户端不会去主动调用下载线程(观察者)的 finish()
方法,而是交给数据中心(被观察对象)去调用。
2.2.1 创建 DownloadTask 类作为观察者
DownloadTask类即为该系统中的观察者,观察者有 id
、loaded
、url
属性,在其上面挂载了一个 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}, {taskId: Number, handler: Function}, ], "订阅事件2": [ {taskId: Number, handler: Function}, {taskId: Number, handler: Function}, ]} }
|
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]) { 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); }
var downloadTask1 = downloadManager.subscribe('dataReady', dataLoader); var downloadTask2 = downloadManager.subscribe('dataReady2', dataLoader);
dataHub.notify('http://somedomain.someaddress', function(url){ downloadManager.publish('dataReady', url); });
dataHub.notify('http://somedomain2.someaddress', function(url){ 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. 区别
观察者模式不需要中间件,被观察对象可以直接将事件通知给观察者。
然而发布订阅模式,则需要一个中间的发布订阅管理器,来进行发布事件与订阅事件的详细方法实现。