javaScript设计模式-Mediator(中介者)模式

Mediator(中介者)模式

如果一个系统的各个组件之间看起来有太多的直接关系,也许是时候需要一个中心控制点了,以便各个组件可以通过这个中心控制点进行通信。

Mediator 模式促进松散耦合的方式是:确保组件的交互是通过这个中心点来处理的,而不是通过显示地引用彼此,这个种模式可以帮助我们解耦系统并提高组件的可重用性。

基本实现

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
var mediator = (function() {
// 存储可被广播或监听的topic
var topics = {};

// 订阅一个topic,提供一个回调函数,一旦topic被广播就执行该回调
var subscribe = function(topic, fn) {
if (!topics[topic]) {
topics[topic] = [];
}
topics[topic].push({
context: this,
callback: fn
});

return this;
};
// 发布/广播事件到程序的剩余部分
var publish = function(topic) {
var args;
if (!topics[topic]) {
return false;
}
// args 保存的是发布的内容
args = Array.prototype.slice.call(arguments, 1);
for (var i = 0, l = topics[topic].length; i < l; i++) {
var subscription = topics[topic][i];
subscription.callback.apply(subscription.context, args);
}
return this;
};

return {
Publish: publish,
Subscribe: subscribe,
installTo: function(obj) {
obj.subscribe = subscribe;
obj.publish = publish;
}
};
})();

高级实现

更高级的代码实现可以浏览 Jack Lawson 的优秀 Mediator.js 的简洁版

首先让我们来实现订阅者的概念,可以考虑一个 Mediator 的 topic 注册实例。

通过生成对象实例,之后我们可以很容易地更新订阅者,而不需要注销并重新注册他们。订阅者可以写成构造函数。该函数接受三个参数:一个可被调用的函数 fn、一个 options 对象和一个 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将context上下文传递给订阅者,默认上下文是window对象
(function(root) {
function guidGenerator() {
/**/
}

// 订阅者构造函数
function Subscriber(fn, options, context) {
if (!this instanceof Subscriber) {
return new Subscriber(fn, context, options);
} else {
// 只给 Subscriber对象挂载这些属性
// guidGenerator() 是用于为订阅者生成GUID的函数,以便之后很方便地引用它们。
// 为了简洁,跳过具体实现

this.id = guidGenerator();
this.fn = fn;
this.options = options;
this.context = context;
this.topic = null;
}
}
})();

Mediator 中的 topic 持有一组回调函数和子 topic 列表,一旦 Mediator.Publish 方法在 Mediator 实例上被调用时,这些回调函数就会被触发。它还包含用于操作数据列表的方法。

一个完整的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<h1>chat</h1>
<form id="chatForm">
<label for="fromBox">Your Name:</label>
<input type="text" name="fromBox" id="fromBox" value="a" />
<br />
<label for="toBox">Send To:</label>
<input type="text" name="toBox" id="toBox" value="b" />
<br />
<label for="chatBox">Message :</label>
<input type="text" name="chatBox" id="chatBox" value="c" />

<button action="submit">Chat</button>
</form>

<div id="chatResult"></div>
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
// 模拟Topic
// Javascript允许我们使用Function对象作为原型的结合与新对象和构造函数一起调用
function Topic(namespace) {
if (!this instanceof Topic) {
return new Topic(namespace);
} else {
this.namespace = namespace;
this._callbacks = [];
this._topics = [];
this.stopped = false;
}
}

// 定义topic的prototype原型,包括添加订阅和获取订阅者的方式
Topic.prototype = {
// 添加新订阅者
AddSubscriber: function(fn, options, context) {
var callback = new window.Mediator.Subscriber(fn, options, context);
this._callbacks.push(callback);
callback.topic = this;
return callback;
},
// ...

// 我们的Topic实例作为一个参数传递给Mediator回调。然后可以 StopPropagation() 的简便方法来调用进一步的回调传播:
StopPropagation: function() {
this.stopped = true;
},

// 当给定GUID标识符时,我们也可以很容易获取现有的订阅者:
GetSubscriber: function(identifier) {
for (var x = 0, y = this._callbacks.length; x < y; x++) {
if (
this._callbacks[x].id == identifier ||
this._callbacks[x].fn == identifier
) {
return this._callbacks[x];
}
}

for (var z in this._topics) {
if (this._topics.hasOwnProperty(z)) {
var sub = this._topics[z].GetSubscriber(identifier);
if (sub !== undefined) {
return sub;
}
}
}
},

// 如果需要它们,我们可以提供简单方法来添加新topic、检查现有topic或者获取topic:
AddTopic: function(topic) {
this._topics[topic] = new Topic(
(this.namespace ? this.namespace + ":" : "") + topic
);
},

HasTopic: function(topic) {
return this._topics.hasOwnProperty(topic);
},

ReturnTopic: function(topic) {
return this._topics[topic];
},

// 如果不在需要订阅者,我们可以显示的删除它们。RemoveSubscriber可以通过它的子主题递归删除一位订阅者:
RemoveSubscriber: function(identifier) {
if (!identifier) {
this._callbacks = [];

for (var z in this._topics) {
if (this._topics.hasOwnProperty(z)) {
this._topics[z].RemoveSubscriber(identifier);
}
}
}
for (var y = 0, x = this._callbacks.length; y < x; y++) {
if (
this._callbacks[y].fn == identifier ||
this._callbacks[y].id == identifier
) {
this._callbacks[y].topic = null;
this._callbacks.splice(y, 1);
x--;
y--;
}
}
},

// 通过topic递归向订阅者发布(Publish)任意参数。
Publish: function(data) {
for (var y = 0, x = this._callbacks.length; y < x; y++) {
var callback = this._callbacks[y],
l;
callback.fn.apply(callback.context, data);
l = this._callbacks.length;

if (l < x) {
y--;
x = 1;
}
}

for (var x in this._topics) {
if (!this.stopped) {
if (this._topics.hasOwnProperty(x)) {
this._topics[x].Publish(data);
}
}
}

this.stopped = false;
}
};

// 这里暴露了我们将主要与之交互的Mediator实例。在这里完成了事件在topic上的注册和移除。
function Mediator() {
if (!this instanceof Mediator) {
return new Mediator();
} else {
this._topics = new Topic("");
}
}

// 对于更高级的使用场景,我们可以让Mediator支持用于inbox:messages:new:read等主题topic的命名空间。
Mediator.prototype = {
GetTopic: function(namespace) {
var topic = this._topics,
namespaceHierarchy = namespace.split(":");

if (namespace === "") {
return topic;
}

if (namespaceHierarchy.length > 0) {
for (var i = 0, j = namespaceHierarchy.length; i < j; i++) {
if (!topic.HasTopic(namespaceHierarchy[i])) {
topic.AddTopic(namespaceHierarchy[i]);
}

topic = topic.ReturnTopic(namespaceHierarchy[i]);
}
}

return topic;
},

// 这里定义了一个Subscribe方法,它接受一个topic命名空间、一个可执行的fn函数、options,以及调用函数的context上下文。如果topic不存在,则创建一个。
Subscribe: function(topicName, fn, options, context) {
var options = options || {},
context = context || {},
topic = this.GetTopic(topicName),
sub = topic.AddSubscriber(fn, options, context);

return sub;
},

// 通过给定的订阅者 ID/命名函数和topic命名空间返回一个订阅者
GetSubscriber: function(identifier, topic) {
console.log(this._topics);
return this.GetTopic(topic || "").GetSubscriber(identifier);
},

// 通过给定的订阅者ID或命名函数,从给定的topic命名空间递归删除订阅者
Remove: function(topicName, identifier) {
console.log(this.GetTopic(topicName));

this.GetTopic(topicName).RemoveSubscriber(identifier);
},

Publish: function(topicName) {
var args = Array.prototype.slice.call(arguments, 1),
topic = this.GetTopic(topicName);

args.push(topic);
this.GetTopic(topicName).Publish(args);
}
};

(function(root) {
function guidGenerator() {
var id = 0;
return (function() {
return id++;
})();
}

function Subscriber(fn, options, context) {
if (!this instanceof Subscriber) {
return new Subscriber(fn, context, options);
} else {
this.id = guidGenerator();
this.fn = fn;
this.context = context;
this.topic = null;
}
}

root.Mediator = Mediator;
Mediator.Topic = Topic;
Mediator.Subscriber = Subscriber;
})(window);

$("#chatForm").on("submit", function(e) {
e.preventDefault();

var text = $("#chatBox").val(),
fromVal = $("#fromBox").val(),
toVal = $("#toBox").val();

mediator.Publish("newMessage", {
message: text,
fromVal,
toVal
});
});

function displayChat(data) {
var date = new Date(),
msg = `${data.fromVal} said ${data.message} to ${data.toVal}`;

$("#chatResult").prepend("" + msg + " (" + date.toLocaleTimeString() + ")");
}

function logChat(data) {
if (window.console) {
console.log(data);
}
}

var mediator = new Mediator();

mediator.Publish("inbox:message:new", {});

mediator.Subscribe("newMessage", displayChat);
mediator.Subscribe("newMessage", logChat);

// 删除前两个订阅者后,将不会在接收到继续发布的消息。
// mediator.Remove('newMessage', 0);
// 重新订阅后,发布消息会被订阅者收到
mediator.Subscribe("newMessage", iAmClearlyCrazy);

function amITalkingToMySelf(data) {
return data.fromVal === data.toVal;
}

function iAmClearlyCrazy(data) {
var flag = amITalkingToMySelf(data);
var str = `<li>${data.fromVal} is talking to ${
flag ? "himself" : "otherPeople"
}</li>`;

$("#chatResult").prepend(str);
}

优点和缺点

优点:
它能够将系统中对象或组件之间所需要的通信渠道从多对多减少到多对一。
缺点:
它会引入单一故障点。将Mediator放置于模块之间会导致性能下降,因为它们总是间接地进行通信。由于松耦合的性质,很难通过关注广播来确定一个系统如何作出反应。

中介者(Mediator)和 观察者(Observer)

在Observer模式中,不存在封装约束单一对象,Observer和Subject必须合作才能维持约束。Communicate(通信)模式由观察者和目标互联的方式所决定:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。

Mediator和Observer都能促进松耦合:然而,Mediator模式通过限制对象严格通过Mediator进行通信来实现这一目的。Observer模式创建观察者对象,观察者对象向订阅它们的对象发布其感兴趣的事件。

中介者(Mediator)与外观(Facade)

Mediator模块在它被模块显示引用的地方汇集这些模块之间的通信。从某种意义说,这是多方向的。另一方面,Facade模式仅是为模块或系统定义了一个较简单的接口,而没有添加任何额外的功能。系统中的其他模块不会直接关联外观,所以可以被视为单向的。