Javascript乱弹设计模式系列(1) - 观察者模式(Observer)

前言

博客园谈设计模式的文章很多,我也受益匪浅,包括TerryLee吕震宇等等的.NET设计模式系列文章,强烈推荐。对于我,擅长于前台代码的开发,对于设计模式也有一定的了解,于是我想结合Javascript来设计前台方面的“设计模式”,以对后台“设计模式”做个补充。开始这个系列我也诚惶诚恐,怕自己写得不好,不过我也想做个尝试,一来希望能给一些人有些帮助吧,二来从写文章中锻炼下自己,三来通过写文章对自己增加自信;如果写得不好,欢迎拍砖,我会虚心向博客园高手牛人们学习请教;如果觉得写得还可以,谢谢大家的支持了:)

这篇将介绍观察者模式。

概述

在现实生活中,存在着“通知依赖关系”,如在报纸订阅的服务,只要读者(订阅者)订购了《程序员》的期刊杂志,那么他就订阅了这个服务,他时刻“监听”着邮递员(出版者)来投递报纸给他们,而邮递员(出版者)只要报社有新刊杂志传达给他(就是状态发生了变化),邮递员(出版者)就随时投递(通知)订阅了服务的读者;另一方面,如果读者不想在继续订购(取消通知)《程序员》的杂志了,那么邮递员就不在投递(通知)这些读者了。---这就是典型的出版者和订阅者的关系,而这个关系用一个公式来概括:

出版者 + 订阅者 = 观察者模式

定义

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都会收到通知并自动更新。

类图

示例分析

现在开始利用观察者模式来应用到项目的一个场景中去,并且层层剖析一下。

有这样一个场景,一个购书网站,用户提交上去一个订单,网站系统只要有订单上来,会采取如下操作:(为了简化,我这里其实只是简单的提交)

一、产生一条通知用户“已购买”记录的短信息(该短信箱还会有其他记录,如交友等等);

二、在浏览器上显示你的订单名片;

三、该条订单提交上服务器,保存到数据库或者其它任何存储介质中去,最后显示你的购书记录;

那么开始我的设计:

1. 网站上添加IPublisher.js,它作为系统的出版者“接口”,利用第0篇文章面向对象基础以及接口和继承类的实现中的Interface.js类(另外,谢谢winter-cn园友提出了些宝贵的建议,目前Interface类还在改善中,这里暂且先用原来的Interface类:

这里是改进的程序示范,包括重载函数的构造,这里也暂时贴出来下:

ContractedBlock.gifExpandedBlockStart.gif改进的代码
改进的代码
// 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;
            }
        )
}

调用如下:

ContractedBlock.gifExpandedBlockStart.gif调用方法
var number = new OverloadNumber();
alert(
"4 - 1 = " + number.Sub(4,1));
alert(
"++3 = " + number.Add(3));
alert(
"4 + 6 = " + number.Add(46));
alert(
"4 + 6 + 5 = " + number.Add(465));
alert(number.Add());
alert(
"3 - 1 = " + number.Sub(3,1));
alert(
"--10 = " + number.Sub(10));
alert(number.Add(
4659)); // 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) - 面向对象基础以及接口和继承类的实现

转载于:https://www.cnblogs.com/liping13599168/archive/2009/01/06/1366599.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/411529.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oracle中出现会话被锁

v$locked_object视图列出当前系统中哪些对象正被锁定 --查询被锁session_id select session_id from v$locked_object; -- SELECT sid, serial#, username, osuser FROM v$session where sid 396; --杀掉一个会话进程 ALTER SYSTEM KILL SESSION 396,17429; 转载于:https://ww…

[html] title与h1、b与strong、i与em的区别分别是什么?

[html] title与h1、b与strong、i与em的区别分别是什么&#xff1f; title标签写在body里面不会被渲染,只能写在head里面,对网站SEO比较重要h1标签写在body里面,但是写在head里(不推荐),渲染的时候会自动渲染到body里面去b标签与strong标签在表现上是一样的,都自带font-weight: …

【JS】JS中数值型字符串相加变成拼接字符串的解决方法

一、问题描述 数值型的字符串&#xff0c;通过、 运算符连接后&#xff0c;变成了字符串拼接&#xff0c;而不是数值计算。 先上图&#xff1a; 二、错误原因 这是由于JS中、 运算符既是算术运算符&#xff0c;也是字符串的连接符> 的运算规则是&#xff1a;如果把数字与字符…

[html] html5都有哪些新的特性?移除了哪些元素?

[html] html5都有哪些新的特性&#xff1f;移除了哪些元素&#xff1f; 新增特性canvassvgvideodrag & droplocalStorage/sessionStorage语义化标签: header/nav/section/article/footerinput 类型: date/datetime/email/range移除元素appletbigfontframe/frameset个人简介…

安卓JAVA调用lua_android中java与lua的相互调用

Android Studio Lua环境配置开发环境1. Android Studio 3.52. java sdk: 1.8.03.android sdk&#xff1a;28配置环境添加lua支持语法支持插件&#xff0c;打开android studio -> File -> Settings三方库选择参考文章&#xff1a;https://gameinstitute.qq.com/community/…

过年之-防飞车贼

1、飞车贼最喜欢在马路边和小巷子里下手。 2、当你在偏僻路段单独行走时&#xff0c;听到后面有靠近的车声&#xff0c;提高警惕&#xff0c;注意避让尾随跟踪、企图接近的摩托车。 3、对于可疑车辆、人员要提高警惕&#xff0c;特别是两个人的摩托车&#xff1a;飞车贼寻找目标…

[html]如何让元素固定在页面底部?有哪些比较好的实践?

[html]如何让元素固定在页面底部&#xff1f;有哪些比较好的实践&#xff1f; *{margin:0;padding:0; } body{height:2000px; } div{width:100%;height:30px;position: fixed;bottom:0;text-align:center;line-height:30px;background: #00CCCC; }个人简介 我是歌谣&#xff…

Windows 2008 R2阿里云安全基线检查

设置密码使用期限策略在管理工具打开本地安全策略&#xff0c;打开路径:安全设置\帐户策略\密码策略&#xff0c;将密码最长使用期限设置为30-180之间&#xff0c;建议值为90&#xff0c;将密码最短使用期限设置为1-14之间&#xff0c;建议值为7.风险账户检查存在可疑隐藏账号&…

用ajax(vb.net) 实现dropdownlist二级无刷新联动~!

说说vb.net ajaxpro 实现dropdownlist二级无刷新联动~&#xff01;Code<div class"div_RSL"> <asp:DropDownList ID"DropDownList1" runat"server" DataSourceID"proID" DataTextField"proName"AutoPostBack"…

[html] xml与html有什么区别?

[html] xml与html有什么区别&#xff1f; xml, 是必须有结束元素<br></br>, 它们是自定义 html, 允许单必合<br />, 它们是已经定义好的 好像头部声明不一样? 其它不记得了, 说实话, 没怎么用过xml个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端…

JAVA遍历文件中的字符串_Java遍历文件夹下所有文件并替换指定字符串

应用场景&#xff1a;比如有一个深层次的文件目录结构&#xff0c;如&#xff1a;javaAPI每个文件里面都有相同的内容&#xff0c;而我们要统一修改为其他内容。上千个文件如果一个个修改显得太不明智。import java.io.BufferedReader;import java.io.File;import java.io.File…

python -m pip install [package] --no-deps

python -m pip install [package] --no-deps 有些 packages 会依赖一些其它的 package&#xff0c;当我们离线安装 whl 的时候&#xff0c;就无法联网下载依赖包&#xff0c;所以我们需要 --no-deps 来去掉依赖包的安装&#xff0c;这样就能离线安装 whl 了 但是 如果 whl 有…

SQLserver被js注入的全库替换SQL

CodeDECLARE fieldtype sysnameSET fieldtypenvarchar--替换处理DECLARE hCForEach CURSOR GLOBALFOR--要替换的列的数据类型SELECT NUPDATE QUOTENAME(o.name)N set QUOTENAME(c.name)REPLACE(QUOTENAME(c.name),<script srchttp://cn.daxia123.cn/cn.js></script>…

[html] 页面中怎么嵌入Flash?有哪些方法?写出来

[html] 页面中怎么嵌入Flash&#xff1f;有哪些方法&#xff1f;写出来 <object width"550" height"400"><param name"movie" value"somefilename.swf"><embed src"somefilename.swf" width"550"…

蓝桥杯java能用编译器1吗_学java的你,这些英文单词都掌握了吗?

1、Abstract class 抽象类:抽象类是不允许实例化的类&#xff0c;因此一般它需要被进行扩展继承。2、Abstract method 抽象方法:抽象方法即不包含任何功能代码的方法。3、Anonymous class 匿名类:当你需要创建和使用一个类&#xff0c;而又不需要给出它的名字或者再次使用的使用…

《高性能网站建设指南》勘误

《高性能网站建设指南》勘误 P5&#xff08;第1章&#xff09;倒数第1段倒数第3行原文&#xff1a;如果你遵从所有适用于你的网站的规则&#xff0c;你的页面的加载速度会提高20%~25%&#xff0c;用户体验也将得到改善。修改&#xff1a;如果你遵从所有适用于你的网站的规则&am…

[html] HTML5如何使用音频和视频?

[html] HTML5如何使用音频和视频&#xff1f; video和audio标签个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

bit不是java基本类型吗_Java bit / byte 基本数据类型

1. bit&#xff1a;位 一个二进制数据0或1&#xff0c;是1bit&#xff1b; 2. byte&#xff1a;字节 存储空间的基本计量单位&#xff0c;如&#xff1a;MySQL中定义 VARCHAR(45) 即是指 45个字节&#xff1b; 1 byte 8 bit 3. 一个英文字符占一个字节&#xff1…

Delphi 与 DirectX 之 DelphiX(35): TDIB.Saturation();

本例效果图:代码文件:unit Unit1;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, DIB, StdCtrls;typeTForm1 class(TForm)DXPaintBox1: TDXPaintBox;Button1: TButton;Button2: TButton;procedure Button1Click(Sender: T…

RabbitMQ基本概念(三)-Centos7下安装RabbitMQ3.6.1

如果你看过前两章对RabbitMQ已经有了一定了解&#xff0c;现在已经摩拳擦掌&#xff0c;来吧动手吧&#xff01; 用什么系统 本文使用的是Centos7&#xff0c;为了保证对linux不太熟悉的伙伴也能轻松上手&#xff08;避免折在安装的路上&#xff09;&#xff0c;下面是我的系统…