前言
博客园谈设计模式的文章很多,我也受益匪浅,包括TerryLee、吕震宇等等的.NET设计模式系列文章,强烈推荐。对于我,擅长于前台代码的开发,对于设计模式也有一定的了解,于是我想结合Javascript来设计前台方面的“设计模式”,以对后台“设计模式”做个补充。开始这个系列我也诚惶诚恐,怕自己写得不好,不过我也想做个尝试,一来希望能给一些人有些帮助吧,二来从写文章中锻炼下自己,三来通过写文章对自己增加自信;如果写得不好,欢迎拍砖,我会虚心向博客园高手牛人们学习请教;如果觉得写得还可以,谢谢大家的支持了:)
这篇将介绍观察者模式。
概述
在现实生活中,存在着“通知依赖关系”,如在报纸订阅的服务,只要读者(订阅者)订购了《程序员》的期刊杂志,那么他就订阅了这个服务,他时刻“监听”着邮递员(出版者)来投递报纸给他们,而邮递员(出版者)只要报社有新刊杂志传达给他(就是状态发生了变化),邮递员(出版者)就随时投递(通知)订阅了服务的读者;另一方面,如果读者不想在继续订购(取消通知)《程序员》的杂志了,那么邮递员就不在投递(通知)这些读者了。---这就是典型的出版者和订阅者的关系,而这个关系用一个公式来概括:
出版者 + 订阅者 = 观察者模式
定义
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都会收到通知并自动更新。
类图
示例分析
现在开始利用观察者模式来应用到项目的一个场景中去,并且层层剖析一下。
有这样一个场景,一个购书网站,用户提交上去一个订单,网站系统只要有订单上来,会采取如下操作:(为了简化,我这里其实只是简单的提交)
一、产生一条通知用户“已购买”记录的短信息(该短信箱还会有其他记录,如交友等等);
二、在浏览器上显示你的订单名片;
三、该条订单提交上服务器,保存到数据库或者其它任何存储介质中去,最后显示你的购书记录;
那么开始我的设计:
1. 网站上添加IPublisher.js,它作为系统的出版者“接口”,利用第0篇文章面向对象基础以及接口和继承类的实现中的Interface.js类(另外,谢谢winter-cn园友提出了些宝贵的建议,目前Interface类还在改善中,这里暂且先用原来的Interface类:
这里是改进的程序示范,包括重载函数的构造,这里也暂时贴出来下:
改进的代码
改进的代码
// Interface.js
function Interface(name, methods)
{
if(arguments.length != 2) {
throw new Error("接口构造函数含" + arguments.length + "个参数, 但需要2个参数.");
}
this.name = name;
this.methods = [];
if(methods.length < 1) {
throw new Error("第二个参数为空数组.");
}
for(var i = 0, len = methods.length; i < len; i++) {
if(typeof methods[i][0] !== 'string') {
throw new Error("接口构造函数第一个参数必须为字符串类型.");
}
for(var j = 1; j < methods[i].length; j++) {
if(methods[i][j] && typeof methods[i][j] !== 'number') {
throw new Error("接口构造函数第二个参数以上必须为整数类型.");
}
}
this.methods.push(methods[i]);
}
};
Interface.registerImplements = function(object) {
if(arguments.length < 2) {
throw new Error("接口的实现必须包含至少2个参数.");
}
for(var i = 1, len = arguments.length; i < len; i++) {
var interface = arguments[i];
if(interface.constructor !== Interface) {
throw new Error("从第2个以上的参数必须为接口实例.");
}
for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
var method1 = interface.methods[j][0];
var arr1 = interface.methods[j].slice(1).sort(compareNumber);
for(var k = 0; k < object.methodArr.length; k++) {
var method2 = object.methodArr[k][0];
if(method1 === method2)
{
var arr2 = object.methodArr[k].slice(1).sort();
if(ComareArray(arr1,arr2))
{
break;
}
else
{
throw new Error("接口的实现对象不能执行" + interface.name + "的接口方法" + method1 + ",因为它找不到或者不匹配.");
}
}
}
}
}
};
function compareNumber(num1, num2) {
var iNum1 = parseInt(num1);
var iNum2 = parseInt(num2);
if(iNum1 < iNum2) return -1;
else if(iNum1 > iNum2) return 1;
else return 0;
}
function ComareArray(arr1, arr2) {
if(arr1.length!=arr2.length)
{
return false;
}
for(var i = 0; i < arr1.length; i++) {
if(arr1[i]!==arr2[i])
{
return false;
}
}
return true;
}
Function.prototype.getParameters = function() {
var str = this.toString();
var paramString = str.slice(str.indexOf('(') + 1, str.indexOf(')')).replace(/\s*/g,''); //取得参数字符串
try
{
return (paramString.length == 0 ? [] : paramString.split(','));
}
catch(err)
{
throw new Error("函数不合法!");
}
}
// demo.js
function Overload(method)
{
this.methods = [];
for(var i = 1; i <= arguments.length - 1; i++)
{
methods.push(arguments[i].length);
}
OverloadNumber.methodArr.push([arguments[0]].concat(methods));
OverloadNumber.argumentArr.push(arguments);
return function()
{
for(var i = 0; i < OverloadNumber.methodArr.length; i++)
{
if(OverloadNumber.methodArr[i][0] == method)
{
var index = OverloadNumber.methodArr[i].slice(1).indexOf(arguments.length);
if(index != -1)
{
return OverloadNumber.argumentArr[i][index+1].apply(this,arguments);
}
}
}
throw new Error("参数不匹配!");
}
}
var INumber = new Interface("INumber", [["Add",1,0,2,3],["Sub",1,2]]); //其中1,0,2,3之类属于重载函数参数个数,可以不按先后顺序
function OverloadNumber() {
Interface.registerImplements(OverloadNumber, INumber);
}
OverloadNumber.methodArr = [];
OverloadNumber.argumentArr = [];
OverloadNumber.prototype = {
Add : Overload( //算术加
"Add",
function(a,b) {
return a + b;
},
function(a) {
return ++a;
},
function(a,b,c) {
return a+b+c;
},
function() {
return -1;
}
),
Sub : Overload( //算术减
"Sub",
function(a) {
return --a;
},
function(a,b) {
return a - b;
}
)
}
调用如下:
调用方法
var number = new OverloadNumber();
alert("4 - 1 = " + number.Sub(4,1));
alert("++3 = " + number.Add(3));
alert("4 + 6 = " + number.Add(4, 6));
alert("4 + 6 + 5 = " + number.Add(4, 6, 5));
alert(number.Add());
alert("3 - 1 = " + number.Sub(3,1));
alert("--10 = " + number.Sub(10));
alert(number.Add(4, 6, 5, 9)); // Error!参数不匹配
)
--------------------
var
IPublisher
=
new
Interface(
'
IPublisher
'
, [[
'
registerSubscriber
'
,
1
], [
'
removeSubscriber
'
,
1
], [
'
notifySubscribers
'
]]);
所有的依赖者(订阅者)将要注册于它的实现类。
2. 添加ISubscriber.js,它作为系统的订阅者“接口”:
var
ISubscriber
=
new
Interface(
'
ISubscriber
'
,[[
'
update
'
,
4
]]);
3. 现在开始实现我们的IPublisher的具体类,添加OrderData.js,它作为一个订单数据类,让其继承IPublisher的接口:
function
OrderData() {
this
._subscribers
=
new
Array();
//
观察者列表
this
.ProductName
=
""
;
//
商品名称
this
.ProductPrice
=
0.0
;
//
商品价格
this
.Recommend
=
0
;
//
推荐指数
this
.productCount
=
0
;
//
购买个数
Interface.registerImplements(
this
, IPublisher);
}
OrderData.prototype
=
{
registerSubscriber :
function
(subscriber) {
//
注册订阅者
this
._subscribers.push(subscriber);
},
removeSubscriber :
function
(subscriber) {
//
删除指定订阅者
var
i
=
_subscribers.indexOf(subscriber);
if
(i
>
0
)
_subscribers.slice(i,
1
);
},
notifySubscribers :
function
() {
//
通知各个订阅者
for
(
var
i
=
0
; i
<
this
._subscribers.length; i
++
)
{
this
._subscribers[i].update(
this
.ProductName,
this
.ProductPrice,
this
.Recommend,
this
.ProductCount);
}
},
SubmitOrder :
function
(productName,productPrice,recommend,productCount) {
//
提交订单
this
.ProductName
=
productName;
this
.ProductPrice
=
productPrice;
this
.Recommend
=
recommend;
this
.ProductCount
=
productCount;
this
.notifySubscribers();
}
}
这里简单介绍下,OrderData构造函数中设置订阅者列表,以及商品属性;
Interface.registerImplements(this, IPublisher); 实际上是让OrderData继承IPublisher接口;
registerSubscriber,removeSubscriber,notifySubscribers实际上覆盖了从IPublisher继承上来的“接口”方法,这样保存了这个类的方法调用,其中notifySubscribers为通知所有的订阅者更新信息;
4. 实现ISubscriber的具体类,添加Subscriber.js,它里面包含三个订阅者,1)MsgBox类,短信箱列表;2)ThisOrder类,该条订单名片;3)OrderList类,我的订单列表;并且让其三都继承ISubscriber的“接口”:
function
MsgBox(publisher)
{
this
.Publisher
=
publisher;
this
.Publisher.registerSubscriber(
this
);
Interface.registerImplements(
this
, ISubscriber);
}
MsgBox.prototype.update
=
function
(productName,productPrice,recommend,productCount) {
//
具体实现
}
function
ThisOrder(publisher)
{
this
.Publisher
=
publisher;
this
.Publisher.registerPublisher(
this
);
Interface.registerImplements(
this
, ISubscriber);
}
ThisOrder.prototype.update
=
function
(productName,productPrice,recommend,productCount) {
//
具体实现
}
function
OrderList(publisher)
{
this
.Publisher
=
publisher;
this
.Publisher.registerPublisher(
this
);
Interface.registerImplements(
this
, ISubscriber);
}
OrderList.prototype.update
=
function
(productName,productPrice,recommend,productCount) {
//
具体实现
}
看到Subscriber实现类们的构造函数中的内容了么?它把出版者类参数赋值于Subscriber实现类们的Publisher对象,然后在该对象上注册this订阅者自己,这样Publisher对象上就注册了Subscriber对象,并且以Array对象的方式存储起来;
5. 好了,IPublisher.js,ISubscriber.js,OrderData.js,Subscriber.js都创建好了,现在需要一个aspx界面来使用它们了:
<
div
id
="Container"
>
<
table
width
="600px"
cellpadding
="0"
cellspacing
="1"
class
="grid"
>
<
thead
>
<
tr
>
<
th
>
商品名
</
th
>
<
th
>
市场价
</
th
>
<
th
>
推荐指数
</
th
>
<
th
>
数量
</
th
>
</
tr
>
</
thead
>
<
tbody
>
<
tr
>
<
td
>
<
span
id
="productName"
>
你必须知道的.NET
</
span
>
</
td
>
<
td
align
="center"
>
<
span
id
="productPrice"
>
69.8
</
span
>
</
td
>
<
td
align
="center"
>
<
span
id
="recommend"
>
10
</
span
>
</
td
>
<
td
align
="center"
>
<
span
id
="productCount"
>
1
</
span
>
</
td
>
</
tr
>
</
tbody
>
<
tfoot
>
<
tr
>
<
td
align
="right"
colspan
="4"
>
<
input
type
="button"
id
="btnSubmit"
value
=" 结 算 "
/>
</
td
>
</
tr
>
</
tfoot
>
</
table
>
</
div
>
<
div
style
="width: 1000px;"
>
<
div
id
="MsgBoxContainer"
>
<
h2
>
您的短信箱
</
h2
>
<
table
width
="100%"
cellspacing
="1"
cellpadding
="0"
class
="grid"
>
<
thead
>
<
tr
>
<
th
>
内容
</
th
>
<
th
style
="width: 100px;"
>
发布日期
</
th
>
</
tr
>
</
thead
>
<
tbody
id
="MsgBoxResult"
>
</
tbody
>
</
table
>
</
div
>
<
div
id
="ThisOrderContainer"
>
<
h2
>
您刚提交的订单名片
</
h2
>
<
div
id
="ThisOrderResult"
>
</
div
>
</
div
>
<
div
id
="OrderListContainer"
>
<
h2
>
您已买的商品列表
</
h2
>
<
table
width
="100%"
cellspacing
="1"
cellpadding
="0"
class
="grid"
>
<
thead
>
<
tr
>
<
th
>
商品名
</
th
>
<
th
>
市场价
</
th
>
<
th
>
推荐指数
</
th
>
<
th
>
数量
</
th
>
<
th
style
="width: 100px;"
>
发布日期
</
th
>
</
tr
>
</
thead
>
<
tbody
id
="OrderListResult"
>
</
tbody
>
</
table
>
</
div
>
<
div
class
="clear"
>
</
div
>
</
div
>
6. 创建一个OrderSend.js,编写相关的JS代码了,以下是核心代码:
$(
"
#btnSubmit
"
).click(
function
(){
var
productName
=
$(
"
#productName
"
).html();
var
productPrice
=
parseFloat($(
"
#productPrice
"
).html());
var
recommend
=
parseInt($(
"
#recommend
"
).html());
var
productCount
=
parseInt($(
"
#productCount
"
).html());
var
orderData
=
new
OrderData();
//
实例化Publisher的实现类orderData
var
msgBox
=
new
MsgBox(orderData);
//
orderData作为MsgBox构造函数的参数进行传递
var
thisOrder
=
new
ThisOrder(orderData);
//
orderData作为ThisOrder构造函数的参数进行传递
var
orderList
=
new
OrderList(orderData);
//
orderData作为OrderList构造函数的参数进行传递
orderData.SubmitOrder(productName,productPrice,recommend,productCount);
//
提交相关商品信息
});
通过点击页面上的“提交”,将三个Subscriber实现类注册到OrderData(Publisher实现类)中去,这样只要OrderData对象提交新商品信息上去,也就是状态更新,那么三个Subscriber实现类就会被通知而更新自身相关的内容了。
页面实现效果如下:
点击“结算”按钮,如下:
这里只是简单的对于三个Subscriber进行更新,关于update方法中的实现,这里不在贴出来了,具体可以下载源代码查看看;在update方法中可以编写你想要的操作以及显示结果,如利用$.ajax进行数据操作和数据展示,这里就留着大家自己发挥吧:)
总结
该篇文章用Javascript来设计观察者模式的思路,通过触发变化通知的方式来请求状态更新,利用一个简单的购书网站来实践。
本篇到此为止,谢谢大家阅读!
附:相关源代码下载
参考文献:《Head First Design Pattern》
本系列文章转载时请注明出处,谢谢合作!
相关系列文章:
Javascript乱弹设计模式系列(6) - 单件模式(Singleton)
Javascript乱弹设计模式系列(5) - 命令模式(Command)
Javascript乱弹设计模式系列(4) - 组合模式(Composite)
Javascript乱弹设计模式系列(3) - 装饰者模式(Decorator)
Javascript乱弹设计模式系列(2) - 抽象工厂以及工厂方法模式(Factory)
Javascript乱弹设计模式系列(1) - 观察者模式(Observer)
Javascript乱弹设计模式系列(0) - 面向对象基础以及接口和继承类的实现