常用的Javascript设计模式
1.策略模式(Strategy)
策略模式定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。
在理解策略模式之前,我们先来一个例子,一般情况下,如果我们要做数据合法性验证,很多时候都是按照swith语句来判断,但是这就带来几个问题,首先如果增加需求的话,我们还要再次修改这段代码以增加逻辑,而且在进行单元测试的时候也会越来越复杂,代码如下:
validator = {
validate: function (value, type) {
switch (type) {
case 'isNonEmpty ':
{
return true; // NonEmpty 验证结果
}
case 'isNumber ':
{
return true; // Number 验证结果
break;
}
case 'isAlphaNum ':
{
return true; // AlphaNum 验证结果
}
default:
{
return true;
}
}
}
};
// 测试
alert(validator.validate("123", "isNonEmpty"));
那如何来避免上述代码中的问题呢,根据策略模式,我们可以将相同的工作代码单独封装成不同的类,然后通过统一的策略处理类来处理,OK,我们先来定义策略处理类,代码如下:
var validator = {
// 所有可以的验证规则处理类存放的地方,后面会单独定义
types: {},
// 验证类型所对应的错误消息
messages: [],
// 当然需要使用的验证类型
config: {},
// 暴露的公开验证方法
// 传入的参数是 key => value对
validate: function (data) {
var i, msg, type, checker, result_ok;
// 清空所有的错误信息
this.messages = [];
for (i in data) {
if (data.hasOwnProperty(i)) {
type = this.config[i]; // 根据key查询是否有存在的验证规则
checker = this.types[type]; // 获取验证规则的验证类
if (!type) {
continue; // 如果验证规则不存在,则不处理
}
if (!checker) { // 如果验证规则类不存在,抛出异常
throw {
name: "ValidationError",
message: "No handler to validate type " + type
};
}
result_ok = checker.validate(data[i]); // 使用查到到的单个验证类进行验证
if (!result_ok) {
msg = "Invalid value for *" + i + "*, " + checker.instructions;
this.messages.push(msg);
}
}
}
return this.hasErrors();
},
// helper
hasErrors: function () {
return this.messages.length !== 0;
}
};
然后剩下的工作,就是定义types里存放的各种验证类了,我们这里只举几个例子:
// 验证给定的值是否不为空
validator.types.isNonEmpty = {
validate: function (value) {
return value !== "";
},
instructions: "传入的值不能为空"
};
// 验证给定的值是否是数字
validator.types.isNumber = {
validate: function (value) {
return !isNaN(value);
},
instructions: "传入的值只能是合法的数字,例如:1, 3.14 or 2010"
};
// 验证给定的值是否只是字母或数字
validator.types.isAlphaNum = {
validate: function (value) {
return !/[^a-z0-9]/i.test(value);
},
instructions: "传入的值只能保护字母和数字,不能包含特殊字符"
};
使用的时候,我们首先要定义需要验证的数据集合,然后还需要定义每种数据需要验证的规则类型,代码如下:
var data = {
first_name: "Tom",
last_name: "Xu",
age: "unknown",
username: "TomXu"
};
validator.config = {
first_name: 'isNonEmpty',
age: 'isNumber',
username: 'isAlphaNum'
};
最后,获取验证结果的代码就简单了:
validator.validate(data);
if (validator.hasErrors()) {
console.log(validator.messages.join("\n"));
}
总结
策略模式定义了一系列算法,从概念上来说,所有的这些算法都是做相同的事情,只是实现不同,他可以以相同的方式调用所有的方法,减少了各种算法类与使用算法类之间的耦合。
从另外一个层面上来说,单独定义算法类,也方便了单元测试,因为可以通过自己的算法进行单独测试。
实践中,不仅可以封装算法,也可以用来封装几乎任何类型的规则,是要在分析过程中需要在不同时间应用不同的业务规则,就可以考虑是要策略模式来处理各种变化。
2.装饰者模式(Decorator)
装饰者提供比继承更有弹性的替代方案。 装饰者用用于包装同接口的对象,不仅允许你向方法添加行为,而且还可以将方法设置成原始对象调用(例如装饰者的构造函数)。
装饰者用于通过重载方法的形式添加新功能,该模式可以在被装饰者前面或者后面加上自己的行为以达到特定的目的。
那么装饰者模式有什么好处呢?前面说了,装饰者是一种实现继承的替代方案。当脚本运行时,在子类中增加行为会影响原有类所有的实例,而装饰者却不然。取而代之的是它能给不同对象各自添加新行为。如下代码所示:
代码如下:
//需要装饰的类(函数)
function Macbook() {
this.cost = function () {
return 1000;
};
}
function Memory(macbook) {
this.cost = function () {
return macbook.cost() + 75;
};
}
function BlurayDrive(macbook) {
this.cost = function () {
return macbook.cost() + 300;
};
}
function Insurance(macbook) {
this.cost = function () {
return macbook.cost() + 250;
};
}
// 用法
var myMacbook = new Insurance(new BlurayDrive(new Memory(new Macbook())));
console.log(myMacbook.cost());
下面是另一个实例,当我们在装饰者对象上调用performTask时,它不仅具有一些装饰者的行为,同时也调用了下层对象的performTask函数。
代码如下:
function ConcreteClass() {
this.performTask = function () {
this.preTask();
console.log('doing something');
this.postTask();
};
}
function AbstractDecorator(decorated) {
this.performTask = function () {
decorated.performTask();
};
}
function ConcreteDecoratorClass(decorated) {
this.base = AbstractDecorator;
this.base(decorated);
decorated.preTask = function () {
console.log('pre-calling..');
};
decorated.postTask = function () {
console.log('post-calling..');
};
}
var concrete = new ConcreteClass();
var decorator1 = new ConcreteDecoratorClass(concrete);
var decorator2 = new ConcreteDecoratorClass(decorator1);
decorator2.performTask();
再来一个彻底的例子:
代码如下:
var tree = {};
tree.decorate = function () {
console.log('Make sure the tree won\'t fall');
};
tree.getDecorator = function (deco) {
tree[deco].prototype = this;
return new tree[deco];
};
tree.RedBalls = function () {
this.decorate = function () {
this.RedBalls.prototype.decorate(); // 第7步:先执行原型(这时候是Angel了)的decorate方法
console.log('Put on some red balls'); // 第8步 再输出 red
// 将这2步作为RedBalls的decorate方法
}
};
tree.BlueBalls = function () {
this.decorate = function () {
this.BlueBalls.prototype.decorate(); // 第1步:先执行原型的decorate方法,也就是tree.decorate()
console.log('Add blue balls'); // 第2步 再输出blue
// 将这2步作为BlueBalls的decorate方法
}
};
tree.Angel = function () {
this.decorate = function () {
this.Angel.prototype.decorate(); // 第4步:先执行原型(这时候是BlueBalls了)的decorate方法
console.log('An angel on the top'); // 第5步 再输出angel
// 将这2步作为Angel的decorate方法
}
};
tree = tree.getDecorator('BlueBalls'); // 第3步:将BlueBalls对象赋给tree,这时候父原型里的getDecorator依然可用
tree = tree.getDecorator('Angel'); // 第6步:将Angel对象赋给tree,这时候父原型的父原型里的getDecorator依然可用
tree = tree.getDecorator('RedBalls'); // 第9步:将RedBalls对象赋给tree
tree.decorate(); // 第10步:执行RedBalls对象的decorate方法
总结
装饰者模式是为已有功能动态地添加更多功能的一种方式,把每个要装饰的功能放在单独的函数里,然后用该函数包装所要装饰的已有函数对象,因此,当需要执行特殊行为的时候,调用代码就可以根据需要有选择地、按顺序地使用装饰功能来包装对象。优点是把类(函数)的核心职责和装饰功能区分开了。
3.代理模式(Proxy)
代理,顾名思义就是帮助别人做事,GoF对代理模式的定义如下:
代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。
代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。
我们来举一个简单的例子,假如dudu要送酸奶小妹玫瑰花,却不知道她的联系方式或者不好意思,想委托大叔去送这些玫瑰,那大叔就是个代理(其实挺好的,可以扣几朵给媳妇),那我们如何来做呢?
// 先声明美女对象
var girl = function (name) {
this.name = name;
};
// 这是dudu
var dudu = function (girl) {
this.girl = girl;
this.sendGift = function (gift) {
alert("Hi " + girl.name + ", dudu送你一个礼物:" + gift);
}
};
// 大叔是代理
var proxyTom = function (girl) {
this.girl = girl;
this.sendGift = function (gift) {
(new dudu(girl)).sendGift(gift); // 替dudu送花咯
}
};
调用方式就非常简单了:
var proxy = new proxyTom(new girl("酸奶小妹"));
proxy.sendGift("999朵玫瑰");
实战一把
通过上面的代码,相信大家对代理模式已经非常清楚了,我们来实战下:我们有一个简单的播放列表,需要在点击单个连接(或者全选)的时候在该连接下方显示视频曲介绍以及play按钮,点击play按钮的时候播放视频,列表结构如下:
<h1>Dave Matthews vids</h1>
<p><span id="toggle-all">全选/反选</span></p>
<ol id="vids">
<li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--2158073">Gravedigger</a></li>
<li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--4472739">Save Me</a></li>
<li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--45286339">Crush</a></li>
<li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--2144530">Don't Drink The Water</a></li>
<li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--217241800">Funny the Way It Is</a></li>
<li><input type="checkbox" checked><a href="http://new.music.yahoo.com/videos/--2144532">What Would You Say</a>
</li>
</ol>
我们先来分析如下,首先我们不仅要监控a连接的点击事件,还要监控“全选/反选”的点击事件,然后请求服务器查询视频信息,组装HTML信息显示在li元素的最后位置上,效果如下:
然后再监控play连接的点击事件,点击以后开始播放,效果如下:
好了,开始,没有jQuery,我们自定义一个选择器:
var $ = function (id) {
return document.getElementById(id);
};
由于Yahoo的json服务提供了callback参数,所以我们传入我们自定义的callback以便来接受数据,具体查询字符串拼装代码如下:
var http = {
makeRequest: function (ids, callback) {
var url = 'http://query.yahooapis.com/v1/public/yql?q=',
sql = 'select * from music.video.id where ids IN ("%ID%")',
format = "format=json",
handler = "callback=" + callback,
script = document.createElement('script');
sql = sql.replace('%ID%', ids.join('","'));
sql = encodeURIComponent(sql);
url += sql + '&' + format + '&' + handler;
script.src = url;
document.body.appendChild(script);
}
};
代理对象如下:
var proxy = {
ids: [],
delay: 50,
timeout: null,
callback: null,
context: null,
// 设置请求的id和callback以便在播放的时候触发回调
makeRequest: function (id, callback, context) {
// 添加到队列dd to the queue
this.ids.push(id);
this.callback = callback;
this.context = context;
// 设置timeout
if (!this.timeout) {
this.timeout = setTimeout(function () {
proxy.flush();
}, this.delay);
}
},
// 触发请求,使用代理职责调用了http.makeRequest
flush: function () {
// proxy.handler为请求yahoo时的callback
http.makeRequest(this.ids, 'proxy.handler');
// 请求数据以后,紧接着执行proxy.handler方法(里面有另一个设置的callback)
// 清楚timeout和队列
this.timeout = null;
this.ids = [];
},
handler: function (data) {
var i, max;
// 单个视频的callback调用
if (parseInt(data.query.count, 10) === 1) {
proxy.callback.call(proxy.context, data.query.results.Video);
return;
}
// 多个视频的callback调用
for (i = 0, max = data.query.results.Video.length; i < max; i += 1) {
proxy.callback.call(proxy.context, data.query.results.Video[i]);
}
}
};
视频处理模块主要有3种子功能:获取信息、展示信息、播放视频:
var videos = {
// 初始化播放器代码,开始播放
getPlayer: function (id) {
return '' +
'<object width="400" height="255" id="uvp_fop" allowFullScreen="true">' +
'<param name="movie" value="http://d.yimg.com/m/up/fop/embedflv/swf/fop.swf"\/>' +
'<param name="flashVars" value="id=v' + id + '&eID=1301797&lang=us&enableFullScreen=0&shareEnable=1"\/>' +
'<param name="wmode" value="transparent"\/>' +
'<embed ' +
'height="255" ' +
'width="400" ' +
'id="uvp_fop" ' +
'allowFullScreen="true" ' +
'src="http://d.yimg.com/m/up/fop/embedflv/swf/fop.swf" ' +
'type="application/x-shockwave-flash" ' +
'flashvars="id=v' + id + '&eID=1301797&lang=us&ympsc=4195329&enableFullScreen=1&shareEnable=1"' +
'\/>' +
'<\/object>';
},
// 拼接信息显示内容,然后在append到li的底部里显示
updateList: function (data) {
var id,
html = '',
info;
if (data.query) {
data = data.query.results.Video;
}
id = data.id;
html += '<img src="' + data.Image[0].url + '" width="50" \/>';
html += '<h2>' + data.title + '<\/h2>';
html += '<p>' + data.copyrightYear + ', ' + data.label + '<\/p>';
if (data.Album) {
html += '<p>Album: ' + data.Album.Release.title + ', ' + data.Album.Release.releaseYear + '<br \/>';
}
html += '<p><a class="play" href="http://new.music.yahoo.com/videos/--' + id + '">» play<\/a><\/p>';
info = document.createElement('div');
info.id = "info" + id;
info.innerHTML = html;
$('v' + id).appendChild(info);
},
// 获取信息并显示
getInfo: function (id) {
var info = $('info' + id);
if (!info) {
proxy.makeRequest(id, videos.updateList, videos); //执行代理职责,并传入videos.updateList回调函数
return;
}
if (info.style.display === "none") {
info.style.display = '';
} else {
info.style.display = 'none';
}
}
};
现在可以处理点击事件的代码了,由于有很多a连接,如果每个连接都绑定事件的话,显然性能会有问题,所以我们将事件绑定在<ol>元素上,然后检测点击的是否是a连接,如果是说明我们点击的是视频地址,然后就可以播放了:
$('vids').onclick = function (e) {
var src, id;
e = e || window.event;
src = e.target || e.srcElement;
// 不是连接的话就不继续处理了
if (src.nodeName.toUpperCase() !== "A") {
return;
}
//停止冒泡
if (typeof e.preventDefault === "function") {
e.preventDefault();
}
e.returnValue = false;
id = src.href.split('--')[1];
//如果点击的是已经生产的视频信息区域的连接play,就开始播放
// 然后return不继续了
if (src.className === "play") {
src.parentNode.innerHTML = videos.getPlayer(id);
return;
}
src.parentNode.id = "v" + id;
videos.getInfo(id); // 这个才是第一次点击的时候显示视频信息的处理代码
};
全选反选的代码大同小异,我们就不解释了:
$('toggle-all').onclick = function (e) {
var hrefs, i, max, id;
hrefs = $('vids').getElementsByTagName('a');
for (i = 0, max = hrefs.length; i < max; i += 1) {
// 忽略play连接
if (hrefs[i].className === "play") {
continue;
}
// 忽略没有选择的项
if (!hrefs[i].parentNode.firstChild.checked) {
continue;
}
id = hrefs[i].href.split('--')[1];
hrefs[i].parentNode.id = "v" + id;
videos.getInfo(id);
}
};
总结
代理模式一般适用于如下场合:
1.远程代理,也就是为了一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实,就像web service里的代理类一样。
2.虚拟代理,根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象,比如浏览器的渲染的时候先显示问题,而图片可以慢慢显示(就是通过虚拟代理代替了真实的图片,此时虚拟代理保存了真实图片的路径和尺寸。
3.安全代理,用来控制真实对象访问时的权限,一般用于对象应该有不同的访问权限。
4.智能指引,只当调用真实的对象时,代理处理另外一些事情。例如C#里的垃圾回收,使用对象的时候会有引用次数,如果对象没有引用了,GC就可以回收它了。
4.工厂模式(Factory)
工厂模式也是对象创建模式之一,它通常在类或类的静态方法中去实现。构造对象的一种方式是使用new操作符,但使用new时正是针对实现编程,会造成“耦合”问题,与具体的类关系紧密。导致代码更脆弱,缺乏弹性,在复杂逻辑的项目中建议是面向接口编程。
先看简单工厂模式
Person(name, age) {
var obj = {}
obj.name = name
obj.age = age
return obj
}
var p1 = Person('jack', 25)
var p2 = Person('lily', 22)
与构造函数方式写一个类的区别在于没有使用this,而是每次都构造一个空对象,然后给其添加属性。创建对象方式不是使用new,而是使用函数调用方式。这种方式基本上用来替代一个类(具有相同属性的对象),而复杂一些的工厂则可以造不同类型的对象。
下面以个水果工厂示例
function Banana() {
this.price = '$1.5'
}
function Apple() {
this.price = '$1.2'
}
function Orange() {
this.price = '$2.2'
}
// 静态工厂类
function Fruit() {}
Fruit.factory = function(type) {
if (!window[type]) {
return
}
var fruit = new window[type]
return fruit
}
// 制造不同的水果
var banana = Fruit.factory('Banana')
var apple = Fruit.factory('Apple')
var orange = Fruit.factory('Orange')
有三个水果类Banana、Apple、Orange,一个水果工厂类Fruit,通过静态方法factory每次可以造出不同的水果类对象。
工厂模式在JavaScript原生对象Object也有所体现,比如
var obj = Object(),
num = Object(1),
str = Object('s'),
boo = Object(false);
Object就是一个工厂,根据参数不同会构造出不同的对象。obj是一个空对象,num是一个Number类型的对象,str是一个String类型的对象,boo是Boolean类型的对象。
jQuery.Callbacks也是一个工厂,每次调用它都会返回一个具有add, remove, fire等方法的对象。还可以根据参数如“once”, “memory”等构造出具有不同性质的对象。
所谓的工厂模式,是指可以返回一个对象的方法。
利用这种模式,我们可以做什么呢?假设我不满足现有的DOM对象里面所拥有的方法,我想要增加一个自定义的方法叫sayHello,我们可以这样做:
function RemouldNodeObj(DomNode){
//先判断一下传递进来的参数是不是一个Dom节点
if(typeof DomNode == "object" && DomNode.nodeType == 1){
DomNode.say = function(){
alert("Hello!!");
}
}else{
alert("你传递进来的参数不正确!");
}
}
//这样调用:
window.onload = function(){
var oDiv = RemouldNodeObj(document.getElementById("test"));
//通过这一步,oDiv就拥有了新的方法say
oDiv.say();
}
有了上面的基础后,我们来实现点复杂的功能,我们要实现只要通过js的调用就生成一个简单的form表单,看代码:
<html>
<head>
<title>JavaScript之工厂模式</title>
<script type="text/javascript">
function RemouldNodeObj(DOMnode){
//先判断一下传递进来的参数是不是一个Dom节点
if(typeof DOMnode == "object" && DOMnode.nodeType == 1){
DOMnode.createForm = function(opt){
//下面是一大串的字符串加法,只是为了拼装出form元素
var oForm = "";
oForm += "<form action=\"" + opt.action + "\" ";
oForm += "method=\"" + (opt.method || 'GET') + "\" id=\"";
oForm += (opt.id || "") + "\"";
oForm += "style=\"width:200px;height:30px;border:2px solid #223344\">";
oForm += "</form>";
//这里的this不要想得太复杂,谁调用就指向谁,所以this指向 oDiv
this.innerHTML = oForm;
}
}else{
alert("参数不正确!");
}
return DOMnode;
}
//这样调用
window.onload = function(){
var oDiv = RemouldNodeObj(document.getElementById("custom"));
oDiv.createForm({
'action' : 'index.jsp',
'method' : 'post',
'id' : 'myForm'
});
}
</script>
</head>
<body>
<div id="custom">###</div>
</body>
</html>
看到了没?这样的调用方式是不是很像jQuery?如果能够解决跨浏览器问题的话,其实完全可以做出一个搜索栏插件来!
5.模板模式(Template)
一、定义
模板方法是基于继承的设计模式,可以很好的提高系统的扩展性。 java中的抽象父类、子类
模板方法有两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。
二、示例
Coffee or Tea
(1) 把水煮沸
(2) 用沸水浸泡茶叶
(3) 把茶水倒进杯子
(4) 加柠檬
/* 抽象父类:饮料 */
var Beverage = function(){};
// (1) 把水煮沸
Beverage.prototype.boilWater = function() {
console.log("把水煮沸");
};
// (2) 沸水浸泡
Beverage.prototype.brew = function() {
throw new Error("子类必须重写brew方法");
};
// (3) 倒进杯子
Beverage.prototype.pourInCup = function() {
throw new Error("子类必须重写pourInCup方法");
};
// (4) 加调料
Beverage.prototype.addCondiments = function() {
throw new Error("子类必须重写addCondiments方法");
};
/* 模板方法 */
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
/* 实现子类 Coffee*/
var Coffee = function(){};
Coffee.prototype = new Beverage();
// 重写非公有方法
Coffee.prototype.brew = function() {
console.log("用沸水冲泡咖啡");
};
Coffee.prototype.pourInCup = function() {
console.log("把咖啡倒进杯子");
};
Coffee.prototype.addCondiments = function() {
console.log("加牛奶");
};
var coffee = new Coffee();
coffee.init();
通过模板方法模式,在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用大多数子类的,但也会出现“个性”子类。
如上述流程,加调料是可选的。
钩子方法可以解决这个问题,放置钩子是隔离变化的一种常见手段。
/* 添加钩子方法 */
Beverage.prototype.customerWantsCondiments = function() {
return true;
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
if(this.customerWantsCondiments()) {
this.addCondiments();
}
}
/* 实现子类 Tea*/
var Tea = function(){};
Tea.prototype = new Beverage();
// 重写非公有方法
Tea.prototype.brew = function() {
console.log("用沸水冲泡茶");
};
Tea.prototype.pourInCup = function() {
console.log("把茶倒进杯子");
};
Tea.prototype.addCondiments = function() {
console.log("加牛奶");
};
Tea.prototype.customerWantsCondiments = function() {
return window.confirm("需要添加调料吗?");
};
var tea = new Tea();
tea.init();
JavaScript没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的。
三、“好莱坞原则”:别调用我们,我们会调用你
典型使用场景:
(1)模板方法模式:使用该设计模式意味着子类放弃了对自己的控制权,而是改为父类通知子类。作为子类,只负责提供一些设计上的细节。
(2)观察者模式:发布者把消息推送给订阅者。
(3)回调函数:ajax异步请求,把需要执行的操作封装在回调函数里,当数据返回后,这个回调函数才被执行。
6.外观模式(Facade)
外观模式(Facade)为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口值得这一子系统更加容易使用。
外观模式不仅简化类中的接口,而且对接口与调用者也进行了解耦。外观模式经常被认为开发者必备,它可以将一些复杂操作封装起来,并创建一个简单的接口用于调用。
外观模式经常被用于JavaScript类库里,通过它封装一些接口用于兼容多浏览器,外观模式可以让我们间接调用子系统,从而避免因直接访问子系统而产生不必要的错误。
外观模式的优势是易于使用,而且本身也比较轻量级。但也有缺点 外观模式被开发者连续使用时会产生一定的性能问题,因为在每次调用时都要检测功能的可用性。
下面是一段未优化过的代码,我们使用了外观模式通过检测浏览器特性的方式来创建一个跨浏览器的使用方法。
var addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false);
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn);
} else {
el['on' + ev] = fn;
}
};
再来一个简单的例子,说白了就是用一个接口封装其它的接口:
var mobileEvent = {
// ...
stop: function (e) {
e.preventDefault();
e.stopPropagation();
}
// ...
};
总结
那么何时使用外观模式呢?一般来说分三个阶段:
首先,在设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade。
其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
第三,在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。
在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定。如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?这就是要说的建造者模式。
建造者模式可以将一个复杂对象的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。也就是说如果我们用了建造者模式,那么用户就需要指定需要建造的类型就可以得到它们,而具体建造的过程和细节就不需要知道了。
这个模式相对来说比较简单,先上代码,然后再解释
function getBeerById(id, callback) {
// 使用ID来请求数据,然后返回数据.
asyncRequest('GET', 'beer.uri?id=' + id, function (resp) {
// callback调用 response
callback(resp.responseText);
});
}
var el = document.querySelector('#test');
el.addEventListener('click', getBeerByIdBridge, false);
function getBeerByIdBridge(e) {
getBeerById(this.id, function (beer) {
console.log('Requested Beer: ' + beer);
});
}
根据建造者的定义,表相即是回调,也就是说获取数据以后如何显示和处理取决于回调函数,相应地回调函数在处理数据的时候不需要关注是如何获取数据的,同样的例子也可以在jquery的ajax方法里看到,有很多回调函数(比如success, error回调等),主要目的就是职责分离。
同样再来一个jQuery的例子:
$('<div class= "foo"> bar </div>');
我们只需要传入要生成的HTML字符,而不需要关系具体的HTML对象是如何生产的。
总结
建造者模式主要用于“分步骤构建一个复杂的对象”,在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化,其优点是:建造者模式的“加工工艺”是暴露的,这样使得建造者模式更加灵活,并且建造者模式解耦了组装过程和创建具体部件,使得我们不用去关心每个部件是如何组装的。
8.观察者模式(Observer)
观察者模式有时也称为发布--订阅模式,在观察者模式中,有一个观察者可以管理所有的目标,等到有状态发生改变的时候发出通知。(其实sql server中的发布订阅也是这个道理)
假如以前村里的广播是一个观察者,那么每个村民就是被观察对象,如果村子里有通知,政策发生改变的时候,就需要通过广播把这个消息发布出去,而不用直接一家家的跑去发通知。
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>观察者模式</title>
</head>
<body>
<script>
var observer = {//观察者
villagers: [],//村名
addVillager: function (callback) {//增加村名
this.villagers[this.villagers.length] = callback;
},
removeVillager: function (callback) {//移除村名
for (var i = 0; i < this.villagers.length; i++) {
if (this.villagers[i] === callback) {
delete this.villagers[i];
}
}
},
publish: function (info) {//发布信息
for (var i = 0; i < this.villagers.length; i++) {
if (typeof this.villagers[i] === 'function') {
this.villagers[i](info);
}
}
},
make: function (o) {//这里将村子建一个这种广播方式
for (var i in this) {
o[i] = this[i];
}
}
};
var village1 = {};
observer.make(village1);//将村子1建立这种观察者模式
var villager11 = {
read: function (what) {
console.log('我是第一个村子的第一个村名:' + what);
}
};
var villager12 = {
read: function (what) {
console.log('我是第一个村子的第二个村名:'+what);
}
};
village1.addVillager(villager11.read);
village1.addVillager(villager12.read);
village1.publish('大家来开会呀!!!');
village1.removeVillager(villager11.read);
village1.publish('大家来开会呀!!!');
/* var village2 = {
myAddVillager:function(callback){
this.addVillager(callback);
},
myRemoveVillager:function(callback){
this.removeVillager(callback);
},
myPublish:function(info){
this.publish(info);
}
};
observer.make(village2);//将村子1建立这种观察者模式
var villager21 = {
read: function (what) {
console.log('我是第二个村子的第一个村名:' + what);
}
};
var villager22 = {
read: function (what) {
console.log('我是第二个村子的第二个村名:'+what);
}
};
village2.myAddVillager(villager21.read);
village2.myAddVillager(villager22.read);
village2.myPublish('大家来领猪肉了!!!');*/
</script>
</body>
</html>
写到这里观察者模式实现了,但是可能会有多个村子需要这种模式,那我们这里将observer改造成构造函数的方式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>观察者模式</title>
</head>
<body>
<script>
function Observer(){//观察者,这里采用构造函数,可以对不同村落进行使用
if(!(this instanceof Observer)){
return new Observer();
}
this.villagers = [];
};
Observer.prototype = {
// villagers: [],//村名
addVillager: function (callback) {//增加村名
this.villagers[this.villagers.length] = callback;
},
removeVillager: function (callback) {//移除村名
for (var i = 0; i < this.villagers.length; i++) {
if (this.villagers[i] === callback) {
delete this.villagers[i];
}
}
},
publish: function (info) {//发布信息
for (var i = 0; i < this.villagers.length; i++) {
if (typeof this.villagers[i] === 'function') {
this.villagers[i](info);
}
}
},
make: function (o) {//这里将村子建一个这种广播方式
for (var i in this) {
o[i] = this[i];
}
}
}
var village1 = {};
var observer1 = new Observer();
observer1.make(village1);//将村子1建立这种观察者模式
var villager11 = {
read: function (what) {
console.log('我是第一个村子的第一个村名:' + what);
}
};
var villager12 = {
read: function (what) {
console.log('我是第一个村子的第二个村名:'+what);
}
};
village1.addVillager(villager11.read);
village1.addVillager(villager12.read);
village1.publish('大家来开会呀!!!');
village1.removeVillager(villager11.read);
village1.publish('大家来开会呀!!!');
var village2 = {
myAddVillager:function(callback){
this.addVillager(callback);
},
myRemoveVillager:function(callback){
this.removeVillager(callback);
},
myPublish:function(info){
this.publish(info);
}
};
var observer2 = new Observer();
observer2.make(village2);//将村子1建立这种观察者模式
var villager21 = {
read: function (what) {
console.log('我是第二个村子的第一个村名:' + what);
}
};
var villager22 = {
read: function (what) {
console.log('我是第二个村子的第二个村名:'+what);
}
};
village2.myAddVillager(villager21.read);
village2.myAddVillager(villager22.read);
village2.myPublish('大家来领猪肉了!!!');
</script>
</body>
</html>
9.抽象工厂模式(Abstract Factory)
抽象工厂模式说明
1. 工厂方法模式的问题: 在工厂方法模式里,创建类都需要通过 工厂类,如果要扩展程序,就必须修改工厂类,这违背了闭包原则,对扩展开放,对修改关闭;对于设计有一定的问题。
2. 如何解决:就要用到抽象工厂模式,就是对功能类单独创建工厂类,这样就不必修改之前的代码,又扩展了功能。
3. 工厂模式其实就是对 实现同一接口的 实现类 的 统一 工厂方式创建调用,但 javascript 没有接口这号东西,所以就去掉这一层 实现,但位功能类的成员及方法都应当一样;
抽象工厂源码例子
1. 邮件发送类:
function MailSender() {
this.to = '';
this.title = '';
this.content = '';
}
MailSender.prototype.send = function() {
//send body
}
2. 短信发送类:
function SmsSender() {
this.to = '';
this.title = '';
this.content = '';
}
SmsSender.prototype.send = function() {
//send body
}
3. 这里本来是创建工厂接口类,这里就去掉了; 直接创建各功能类工厂;
1>. 邮件工厂类:
function MailFactory() {
}
MailFactory.prototype.produce = function() {
return new MailSender();
}
2>. 短信工厂类:
function SmsFactory() {
}
SmsFactory.prototype.produce = function() {
return new SmsSender();
}
4. 使用方法:
var factory = new MailFactory();
var sender = factory.produce();
sender.to = 'toname#mail.com';
sender.title = '抽象工厂模式';
sender.content = '发送内容';
sender.send();
其他说明
在面向对象语言如 java,.net C# 使用的工厂模式,都用到接口,接口是对外向各种用户暴露的可用方法,说明这个功能应用有些什么的方法应用,用户应该怎么用这个接口。对象以类的形式表现出来,代表现实世界中的某种抽象,也许场景会有很多类似的应用,比如上面的 邮件发送,短信发送,再比如商场中的各种促销手段,以及动物世界中的各种飞禽走兽等..
如果我们不以接口形式提供用户使用,势必提供暴露真实的功能类对象给用户,用户可以随意对类对象进行修改跟扩展,这是不允许的。
工厂方法模式 跟 抽象工厂模式可以很好的解决这样的问题,用户只能使用接口调用工厂类,来进行规定的操作;抽象工厂模式更进一步使用扩展功能变得容易,功能类跟工厂类都在实现相应的接口上实现各自类级别的扩展,不会涉及修改到其他的类或方法;
10.适配器模式(Adapter)
说明: 适配器模式,一般是为要使用的接口,不符本应用或本系统使用,而需引入的中间适配层类或对象的情况;
场景: 就好比我们买了台手机,买回来后发现,充电线插头是三插头,但家里,只有两插头的口的插座,怎么办?为了方便,也有为能在任何地方都能充上电,就得去买个通用充电适配器; 这样手机才能在自己家里充上电;不然只能放着,或跑到有这个插头的地方充电;
实际开发环境下,由于旧的系统,或第三方应用提供的接口,与我们定义的接口不匹配,在以面向接口编程的环境下,就无法使用这样旧的,或第三方的接口,这时我们就使用适配类继承待适匹配的类,并让适配类实现接口的方式来引入旧的系统或第三方应用的接口;
这样使用接口编程时,就可以使用这个适匹配类,来间接调用旧的系统或第三方应用的接口。
在 Javascript 要实现类似动态面向对象语言的适配器模式的代码,可以使用到 prototype 的继承实例来实现;因为是基于接口约束的,但是Javascript没有接口这号东西,我们去掉接口这一层,直接实现接口实现类 Target ,模拟类似的源码出来;
源码实例
1. 待适配的类及接口方法:
代码如下:
function Adaptee() {
this.name = 'Adaptee';
}
Adaptee.prototype.getName = function() {
return this.name;
}
2. 普通实现类 [由于 Javascript 中没有接口,所以就直接提供实现类]
function Target() {
this.name = 'Target';
}
Target.prototype.queryName= function() {
return this.name;
}
3. 适配类:
function Adapte() {
this.name = '';
}
Adapte.prototype = new Adaptee();
Adapte.prototype.queryName = function() {
this.getName();
}
4.使用方法:
var local = new Target();
local.queryName(); //调用普通实现类
var adapte = new Adapte();
adapte.queryName(); //调用旧的系统或第三方应用接口;
其他说明
上面第四步,var local 以及 var adapte 类似像 Java,C# 这样的面向对象语言中接口引用指定,如:
interface Target {
public String queryName();
}
//接口引用指向
Target local = new RealTarget(); //即上面 Javascript 的 Target 实现类
local.queryName();
//适配器
Target adapte = new Adapte();
adapte.queryName();
可见适配器类是连接接口与目标类接口的中间层;就是用来解决,需要的目标已经存在了,但我们无法直接使用,不能跟我们的代码定义协同使用,就得使用适器模式,适配器模式也叫转换模式,包装模式;
11.单例模式(Singleton)
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器的window对象。在js开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录框,而这个浮窗是唯一的,无论单击多少次登录按钮,这个浮窗只会被创建一次。因此这个登录浮窗就适合用单例模式。
1、单例模式的使用场景
在使用一种模式之前,我们最好要知道,这种模式的使用场景。用了这么久的单例模式,竟全然不知!用它具体有哪些好处呢?
1).可以用它来划分命名空间(这个就是就是经常用的了)
2).利用分支技术来封装浏览器之间的差异(这个还真没用过,挺新鲜)
3).借助单例模式,可以把代码组织的更为一致,方便阅读与维护(这个也用过了)
2、最基本的单例模式
最简单的单例其实就是一个对象字面量。它把一批有一定关联的方法和属性组织在一起。
var Singleton = {
attr1: true ,
attr2: 10 ,
method1 : function(){
alert('我是方法1');
},
method2 : function(){
alert('我是方法2');
}
};
这个对象可以被修改。你可以添加属性和方法。你也可以用delete运算符删除现有成员。这实际上违背了面向对象设计的一条原则:类可以被扩展,但不应该被修改。如果某些变量需要保护,那么可以将其定义在闭包中。
对象字面量只是创建单例的方法之一。也并非所有的对象字面量都是单例,那些只是用来模仿关联数组或容纳数据的对象字面量显然不是单例。
3、借用闭包创建单例
闭包主要的目地 保护数据
// 命名空间
var BHX = {} ;
BHX.Singleton = (function(){
// 添加自己的私有成员
var a1 = true ;
var a2 = 10 ;
var f1 = function(){
alert('f1');
}
var f2 = function(){
alert('f2');
}
// 把块级作用域里的执行结果赋值给我的单例对象
return {
attr1: a1 ,
attr2: a2 ,
method1 : function(){
return f1();
},
method2 : function(){
return f2();
}
} ;
})();
alert(BHX.Singleton.attr1);
BHX.Singleton.method1();
这种单例模式又称模块模式,指的是它可以把一批相关的方法和属性组织为模块并起到划分命名空间的作用。
4、单例模式用于划分命名空间
1)、防止全局声明的修改
/*using a namespace*/
var BHX = {};
BHX.Singleton = {
attr1: true ,
attr2: 10 ,
method1 : function(){
alert('我是方法1');
},
method2 : function(){
alert('我是方法2');
}
};
BHX.Singleton.attr1;
var attr1 = false;
这样以来,即使我们在外面声明了相同的变量,也能在一定程度上防止attr1的被修改。
2)、防止其它来源代码的修改
现在网页上的JavaScript代码往往不止用一个来源,什么库代码、广告代码和徽章代码。为了避免与自己代码的冲突,可以定义一个包含自己所有代码的对象。
var XGP = {};
XGP.Common = {
//A singleton with common methods used by all objects and modules
}
XGP.ErrorCodes = {
//An object literal used to store data
}
XGP.PageHandler = {
//A singleton with page specific methods and attributes.
}
3)、用作专用代码封装
在拥有许多网页的网站中,有些代码是所有网页都要用到的,他们通常被存放在独立的文件中;而有些代码则是某个网页专用的,不会被用到其他地方。最好把这两种代码分别包装在自己的单例对象中。
我们经常要用Javascript为表单添加功能。出于平稳退化方面的考虑,通常先创建一个不依赖于Javascript的、使用普通提交机制完成任务的纯HTML网页。
XGP.RegPage = {
FORM_ID: 'reg-form',
OUTPUT_ID: 'reg-result',
handleSubmit: function(e){
e.preventDefault(); //stop the normal form submission
var data = {};
var inputs = XGP.RegPage.formEl.getElementByTagName('input');
for(var i=0, len=inputs.length; i<len; i++){
data[inputs[i].name] = inputs[i].value;
}
XGP.RegPage.sendRegistration(data);
},
sendRegistration: function(data){
//make an xhr request and call displayResult() when response is recieved
...
},
displayResult: function(response){
XGP.RegPage.outputEl.innerHTML = response;
},
init: function(){
XGP.RegPage.formEl =$(XGP.RegPage.Form_ID);
XGP.RegPage.outputEl = $(XGP.RegPage.OUTPUT_ID);
//hijack the form submission
addEvent(XGP.RegPage.formEl, 'submit', XGP.RegPage.handleSubmit);
}
}
//invoke initialization method after the page load
addLoadEvent(XGP.RegPage.init);
5、惰性单例
前面所讲的单例模式又一个共同点:单例对象都是在脚本加载时被创建出来。对于资源密集的或配置开销甚大的单例,更合理的做法是将其实例化推迟到需要使用他的时候。
这种技术就是惰性加载(lazy loading)。
实现步骤如下:
1).将所有代码移到constructor方法中
2).全权控制调用时机(正是getInstance所要做的)
XGP.lazyLoading = (function(){
var uniqInstance;
function constructor(){
var attr = false;
function method(){
}
return {
attrp: true,
methodp: function(){
}
}
}
return {
getInstance: function(){
if(!uniqInstance){
uniqInstance = constructor();
}
return uniqInstance;
}
}
})();
6、分支技术
分支是一种用来把浏览器间的差异封装在运行期间进行设置的动态方法中的技术。
// 分支单例 (判断程序的分支 <浏览器差异的检测>)
var Ext = {} ;
var def = false ;
Ext.More = (function(){
var objA = { // 火狐浏览器 内部的一些配置
attr1:'FF属性1'
// 属性1
// 属性2
// 方法1
// 方法2
} ;
var objB = { // IE浏览器 内部的一些配置
attr1:'IE属性1'
// 属性1
// 属性2
// 方法1
// 方法2
} ;
return (def) ?objA:objB;
})();
alert(Ext.More.attr1);
比如说,如果网站中要频繁使用xhr,每次调用都要再次运行浏览器嗅探代码,这样会严重缺乏效率。更有效的做法是在脚本加载时一次性地确定针对浏览器的代码。这正是分支技术所做的事情。当然,分支技术并不总是更高效的选择,在两个或者多个分支中只有一个分支被用到了,其他分支就占用了内存。
在考虑是否使用分支技术的时候,必须在缩短时间和占用更多内存这一利一弊之间权衡一下。
下面利用分支技术实现XHR:
var XHR = (function(){
var standard = {
createXhrObj: function(){
return new XMLHttpRequest();
}
};
var activeXNew = {
createXhrObj: function(){
return new ActiveXObject('Msxml2.XMLHTTP');
}
};
var activeXOld = {
createXhrObj: function(){
return new ActiveXObject('Microsoft.XMLHTTP');
}
};
var testObj;
try{
testObj = standard.createXhrObj();
return testObj;
}catch(e){
try{
testObj = activeXNew.createXhrObj();
return testObj;
}catch(e){
try{
testObj = activeXOld.createXhrObj();
return testObj;
}catch(e){
throw new Error('No XHR object found in this environment.');
}
}
}
})();
7、单例模式的弊端
了解了这么多关于单例的知识,我们再来看看它的弊端。
由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合。因此也就不利于单元测试了。
综上,单例还是留给定义命名空间和实现分支型方法这些用途。
通过七点不同方面对单例模式的介绍,大家是不是对单例模式有了更深入的了解,希望这篇文章可以帮到大家。
12.命令模式(Command)
命令模式(Command)的定义是:用于将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及执行可撤销的操作。也就是说改模式旨在将函数的调用、请求和操作封装成一个单一的对象,然后对这个对象进行一系列的处理。此外,可以通过调用实现具体函数的对象来解耦命令对象与接收对象。
我们来通过车辆购买程序来展示这个模式,首先定义车辆购买的具体操作类:
$(function () {
var CarManager = {
// 请求信息
requestInfo: function (model, id) {
return 'The information for ' + model +
' with ID ' + id + ' is foobar';
},
// 购买汽车
buyVehicle: function (model, id) {
return 'You have successfully purchased Item '
+ id + ', a ' + model;
},
// 组织view
arrangeViewing: function (model, id) {
return 'You have successfully booked a viewing of '
+ model + ' ( ' + id + ' ) ';
}
};
})();
来看一下上述代码,通过调用函数来简单执行manager的命令,然而在一些情况下,我们并不想直接调用对象内部的方法。这样会增加对象与对象间的依赖。现在我们来扩展一下这个CarManager 使其能够接受任何来自包括model和car ID 的CarManager对象的处理请求。根据命令模式的定义,我们希望实现如下这种功能的调用:
CarManager.execute({ commandType: "buyVehicle", operand1: 'Ford Escort', operand2: '453543' });
根据这样的需求,我们可以这样啦实现CarManager.execute方法:
CarManager.execute = function (command) {
return CarManager[command.request](command.model, command.carID);
};
改造以后,调用就简单多了,如下调用都可以实现(当然有些异常细节还是需要再完善一下的):
CarManager.execute({ request: "arrangeViewing", model: 'Ferrari', carID: '145523' });
CarManager.execute({ request: "requestInfo", model: 'Ford Mondeo', carID: '543434' });
CarManager.execute({ request: "requestInfo", model: 'Ford Escort', carID: '543434' });
CarManager.execute({ request: "buyVehicle", model: 'Ford Escort', carID: '543434' });
总结
命令模式比较容易设计一个命令队列,在需求的情况下比较容易将命令计入日志,并且允许接受请求的一方决定是否需要调用,而且可以实现对请求的撤销和重设,而且由于新增的具体类不影响其他的类,所以很容易实现。
但敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能,如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需求的时通过重构实现这个模式并不困难,只有在真正需求如撤销、恢复操作等功能时,把原来的代码重构为命令模式才有意义。