设计模式使用场景分析

代理模式

  • 虚拟代理图片预加载
    如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。
var myImage = (function() {
	var imgNode = document.createElement('img');
	document.body.appendChild(imgNode);
	return {
		setSrc: function(src) {
			imgNode.src = src;
		}
	}
})();
var proxyImage = (function() {
	var img = new Image;
	img.onload = function() {
		//等真正图片加载好了之后,再改变成真实图片
		myImage.setSrc(this.src);
	}
	return {
		setSrc: function(src) {
			//先设置一张缓存图片
			myImage.setSrc('img/loading.gif');
			img.src = src;
		}
	}
})();
proxyImage.setSrc('img/1.jpg');
  • 虚拟代理合并 HTTP 请求
    假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同
    步到另外一台备用服务器上面。当我们选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。可以预见,如此频繁的网络请求将会带来相当大的开销。
    解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统, 2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。代码如下:
var synchronousFile = function(id) {
	console.log('开始同步文件, id 为: ' + id);
};
var proxySynchronousFile = (function() {
	var cache = [], // 保存一段时间内需要同步的 ID
		timer; // 定时器
	return function(id) {
		cache.push(id);
		if(timer) { // 保证不会覆盖已经启动的定时器
			return;
		}
		timer = setTimeout(function() {
			synchronousFile(cache.join(',')); // 2 秒后向本体发送需要同
			clearTimeout(timer); // 清空定时器
			timer = null;
			cache.length = 0; // 清空 ID 集合
		}, 2000);
	}
})();
var checkbox = document.getElementsByTagName('input');
for(var i = 0, c; c = checkbox[i++];) {
	c.onclick = function() {
		if(this.checked === true) {
			proxySynchronousFile(this.id);
		}
	}
};

###责任链模式
一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象。请求发送者只需要知道链中的第一个节点。ps:可以将业务代码从if/else中释放出来,便于程序的扩展。
业务例子:公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券, 200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

/*职责链 */
/* orderType:表示订单类型(定金用户或者普通购买用户), code 的值为 1 的时候是 500 元
                            定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
 pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的
                订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
 stock:表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用
户不受此限制
*/
var order500 = function(orderType, pay, stock) {
	if(orderType === 1 && pay === true) {
		console.log('500 元定金预购,得到 100 优惠券');
	} else {
		return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
	}
};
var order200 = function(orderType, pay, stock) {
	if(orderType === 2 && pay === true) {
		console.log('200 元定金预购,得到 50 优惠券');
	} else {
		return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
	}
};
var orderNormal = function(orderType, pay, stock) {
	if(stock > 0) {
		console.log('普通购买,无优惠券');
	} else {
		console.log('手机库存不足');
	}
};
var Chain = function(fn) {
	this.fn = fn;
	this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor) {
	return this.successor = successor;
};
Chain.prototype.passRequest = function() {
	var ret = this.fn.apply(this, arguments);
	if(ret === 'nextSuccessor') {
		return this.successor && this.successor.passRequest.apply(this.successor, arguments);
	}
	return ret;
};
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
//然后指定节点在职责链中的顺序:
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
chainOrder500.passRequest( 1, true, 500 ); // 输出: 500 元定金预购,得到 100 优惠券
chainOrder500.passRequest( 2, true, 500 ); // 输出: 200 元定金预购,得到 50 优惠券
chainOrder500.passRequest( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 ); // 输出:手机库存不足

命令模式

  • 撤销和重做
  • 播放和录像功能
  • 宏命令
    ###中介者模式
    试想一下这个场景:一个手机购买界面,有选择手机颜色,大小,型号等下拉框及输入框。每当用户进行操作后,要实时回显用户的操作,并且在某一项未选择的时候,确认按钮应该是被禁用状态。
    如果把这些内容都写在操作的回调函数中,比如:在选择手机颜色onselect事件中,写很多逻辑代码。那这样就不利于程序的扩展。在以后新增了某个选项,则需要在每个输入框的回调函数中增加对此新增的下拉框的判断。
    所以我们应该把逻辑代码分离出来。交由中介者去完成判断。对于新增的业务,也只需要改中介者这个对象中的业务代码。在所有输入框或下拉框的回调事件中,我们只需向中介者发个请求,告诉它,我现在变化了。中介者接收到这个信号后,会自动做一系列的处理判断。代码如下:
var goods = { // 手机库存
	"red|32G": 3,
	"red|16G": 0,
	"blue|32G": 1,
	"blue|16G": 6
};
var mediator = (function() {
	var colorSelect = document.getElementById('colorSelect'),
		memorySelect = document.getElementById('memorySelect'),
		numberInput = document.getElementById('numberInput'),
		colorInfo = document.getElementById('colorInfo'),
		memoryInfo = document.getElementById('memoryInfo'),
		numberInfo = document.getElementById('numberInfo'),
		nextBtn = document.getElementById('nextBtn');
	return {
		changed: function(obj) {
			var color = colorSelect.value, // 颜色
				memory = memorySelect.value, // 内存
				number = numberInput.value, // 数量
				stock = goods[color + '|' + memory]; // 颜色和内存对应的手机库存数量
			if(obj === colorSelect) { // 如果改变的是选择颜色下拉框
				colorInfo.innerHTML = color;
			} else if(obj === memorySelect) {
				memoryInfo.innerHTML = memory;
			} else if(obj === numberInput) {
				numberInfo.innerHTML = number;
			}
			if(!color) {
				nextBtn.disabled = true;
				nextBtn.innerHTML = '请选择手机颜色';
				return;
			}
			if(!memory) {
				nextBtn.disabled = true;
				nextBtn.innerHTML = '请选择内存大小';
				return;
			}
			if(((number - 0) | 0) !== number - 0) { // 输入购买数量是否为正整数
				nextBtn.disabled = true;
				nextBtn.innerHTML = '请输入正确的购买数量';
				return;
			}
			nextBtn.disabled = false;
			nextBtn.innerHTML = '放入购物车';
		}
	}
})();
// 事件函数:
colorSelect.onchange = function() {
	mediator.changed(this);
};
memorySelect.onchange = function() {
	mediator.changed(this);
};
numberInput.oninput = function() {
	mediator.changed(this);
};

发布-订阅模式

适用场景:一个网站,有好几个模块都需要获取用户信息后,进行渲染显示。在未用此种设计模式之前,我们会想着在获取用户信息的ajax成功回调函数中,写代码,渲染各个模块。但是这样做会导致,我们新增一个模块,需要去修改之前ajax的回调函数,加入新增部分的处理。而一个团队中,可能ajax回调函数部分是同事A做的,而新增模块是同事B负责的,其实B具体要做什么事情,A并不需要知道,A只需要告诉你,我已经拿到用户信息了,你们想干嘛就干嘛吧。那么这时候A提供出一个接口供其他需要它的人来订阅,在A完成任务后,告诉之前这些跟它打过招呼的模块,我把这些对你们有用信息传给你们,具体后续的事情由你们自己来决定。这就是发布-订阅模式。
###组合模式
这种模式非常适用于树形结构

  • 扫描文件夹
/******************************* Folder ******************************/
var Folder = function (name) {
    this.name = name;
    this.files = [];
};
Folder.prototype.add = function (file) {
    this.files.push(file);
};
Folder.prototype.scan = function () {
    console.log('开始扫描文件夹: ' + this.name);
    for (var i = 0, file, files = this.files; file = files[i++];) {
        file.scan();
    }
};
/******************************* File ******************************/
var File = function (name) {
    this.name = name;
};
File.prototype.add = function () {
    throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function () {
    console.log('开始扫描文件: ' + this.name);
};
var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var folder2 = new Folder('jQuery');
var file1 = new File('JavaScript 设计模式与开发实践');
var file2 = new File('精通 jQuery');
var file3 = new File('重构与模式')
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
var folder3 = new Folder('Nodejs');
var file4 = new File('深入浅出 Node.js');
folder3.add(file4);
var file5 = new File('JavaScript 语言精髓与编程实践');
folder.add(folder3);
folder.add(file5);
folder.scan();

###装饰者模式

  • 装饰方法
    当我们在扩展方法的时候,更保险的方法是将原来的方法保留下来,在新的方法中再执行一次原来的方法。比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们一般都会先保存好原先的 window.onload,把它放入新的 window.onload 里执行:如:
window.onload = function() {
	alert(1);
}
var _onload = window.onload || function() {};
window.onload = function() {
	_onload();
	alert(2);
}

但是上面会存在一些问题。1、 例如丢失了this指向。2、 需维护_onload 这个中间变量,如果函数的装饰链够长,所需的中间变量越多。

  • 面向AOP编程,为方法添加前执行函数和后执行函数。
 Function.prototype.before = function(beforefn) {
	var __self = this; // 保存原函数的引用
	return function() { // 返回包含了原函数和新函数的"代理"函数
		beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
		// 也会被原封不动地传入原函数,新函数在原函数之前执行
		return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
		// 并且保证 this 不被劫持
	}
}
Function.prototype.after = function(afterfn) {
	var __self = this;
	return function() {
		var ret = __self.apply(this, arguments);
		afterfn.apply(this, arguments);
		return ret;
	}
};

状态模式

你可能感兴趣的:(设计模式)