设计模式(策略)
这次我们讲的设计模式叫做 Strategy, 策略模式
策略这个名字听上去,就像是预谋好的。也就是说,所有的解决方案提前设定好
不再根据当下的情形做判断,而是将不同问题与不同的解决方案对应起来。
举个简单的例子
比如,周一到周三,你上早班,早上要定闹铃,大概7点钟
周四到周五,晚班,闹铃可以晚一些,订在9点半
而周六是不用上班的,所以闹铃取消。
正常情况下,我们会在每天晚上判断一下明天上什么班,然后再去订闹铃。
但是现在的智能手机,已经使用策略模式帮我们解决了问题:
周一(早班) ------> 7:00
周二(早班) ------> 7:00
周三(早班) ------> 7:00
周四(晚班) ------> 9:30
周五(晚班) ------> 9:30
周六(休息) ------> 无
周日(休息) ------> 无
(上图为锤子手机设置闹铃截图,每个闹铃都可以分别设定日期或跟随节假日自动调整, @罗永浩 请把广告费用结算一下)
所以每当周一的时候,系统自动调取周一对应的闹铃方案,也就是7:00的闹铃
所以,策略模式最大的好处,就在于将问题和方案进行了映射关系处理
这样以后就省去了
“这个问题该用哪个方案”的判断
代码:
var
TankFactory
=
{
tanklist : {
"幻影"
:
function
(){
//......
},
"光棱"
:
function
(){
//......
},
"爱国者"
:
function
(){
//......
},
"犀牛"
:
function
(){
//......
},
"天启"
:
function
(){
//......
}
},
create :
function
(
type
){
return new
this
.tanklist[
type
]();
//大家注意这个地方tanklist[type]的使用,就是一种策略模式
}
};
|
再比如:
我们经常会遇到多分支判断的情况
|
function
start
(
level
){
switch
(level){
case
1
: {
todo1();
break
;
},
case
2
: {
todo2();
break
;
},
case
3
: {
todo3();
break
;
},
case
4
: {
todo4();
break
;
},
}
}
|
我们可以将代码改造如下:
|
var
start
=
(
function
(){
var
todolist
=
{
1
:
function
(){
//todo1....
},
2
:
function
(){
//todo2....
},
3
:
function
(){
//todo3....
},
4
:
function
(){
//todo4....
}
}
return
function
(
level
){
todolist[
level
]();
//省去了判断,直接调用对应函数
}
})();
|
实战练习: 表单验证
HTML代码
|
<
p
>
<
label
>用户名:</
label
>
<
input
type
=
"text"
name
=
"username"
/>
</
p
>
<
p
>
<
label
>密码:</
label
>
<
input
type
=
"text"
name
=
"password"
/>
</
p
>
<
p
>
<
label
>邮箱:</
label
>
<
input
type
=
"text"
name
=
"email"
/>
</
p
>
<
p
>
<
input
type
=
"submit"
value
=
"提交"
/>
</
p
>
</
form
>
|
验证工具
|
var
Validator
=
(
function
(){
var
rules
=
{
notEmpty :
function
(
val
){
return
val
!=
""
&&
val
!=
null
&&
val
!=
undefined
;
},
lengthRange :
function
(
val
,
args
) {
var
[min,max]
=
[args.split(
","
)[
0
], args.split(
","
)[
1
]];
return
val.length
>=
min
&&
val.length
<=
max;
},
email :
function
(
val
){
return
/^\w+@\w+(\.\w+)*$/
.test(val);
}
}
return
{
check :
function
(
val
,
sets
){
return
sets.
every
(
function
(
rulename
){
return
rules[rulename.split(
"\|"
)[
0
]](val, rulename.split(
"\|"
)[
1
]);
});
}
};
})();
|
使用验证工具校验表单
|
var
form
=
document
.querySelector(
"#regForm"
);
form.
onsubmit
=
function
(){
try
{
var
res
=
Validator.check(form.username.value,[
"notEmpty"
,
"lengthRange|6,20"
]);
console
.log(res);
}
catch
(e) {
console
.log(e);
}
return
false
;
}
|
设计模式(代理)
这次我们讲的设计模式叫做 Proxy, 代理模式
那么代理模式是什么呢? 举个例子
明星----》经纪人----》代理与被代理, 拍电影,真人秀 (谈判、行程安排、炒作)
代码:
function SuperStar(){
this.拍电影= function(){
}
this.真人秀= function(){
}
}
function Proxy(){
this.主人 = new SuperStart();
this.找我主人拍电影 = functioin(){
//谈价格、谈档期、谈待遇、确定行程计划
if(一切OK){
this.主人.拍电影();
}
}
this.找我主人演真人秀 = function(){
//谈价格、谈档期、谈待遇、确定行程计划
if(一切ok){
this.主人.真人秀();
}
}
}
实战练习: 加载图片时的延迟,我们希望真正的图片在加载完成之前都显示一个类似loading的图片,
当真的图片完成下载后,再替换掉loading图片
//非代理模式
function CreateImage(){
var img = document.create("img");
document.body.appendChild(img);
img.src = "loading.jpg"; //loading图片
var cacheImg = new Image();
cacheImg.onload = function(){
img.src = this.src;//当缓存对象加载完成后,将真正的图片放入img
}
this.setSrc = function(url){
cacheImg.src = url; //使用缓存对象加载真正的图片
}
}
这样做虽然可以达到效果,但明显CreateImage函数过于复杂,所以现在要简化函数的内容
让CreateImage职责更单一,只负责加载图片。就可以使用代理模式
代码:
function CreateImage(){
var img = document.create("img");
document.body.appendChild(img);
img.src = "loading.jpg";
this.setSrc = function(url){
this.src = url;
}
}
//代理
function ProxyImage(){
var target = new CreateImage();
var img = new Image();
img.onload = function(){
target.src = this.src;
}
this.setSrc = function(url){
img.src = url;
}
}
var img = new ProxyImage();
img.setSrc = "realimg.jpg";
实战练习: 如何给一个ajax请求增加缓存?
假设现在有一个ajax请求函数:
function ajax(type, url, success){
var req = new XMLHttpRequest();
req.open(type, url, sync);
req.onreadystatechange = function(){
if(req.status == 200 && req.readyState == 4){
success.call(null, req.responseText);
}
}
req.send();
}
增加缓存机制,每个请求过的ajax,5分钟内再次请求,从缓存中直接获取数据不再像服务器发送请求。
var proxyAjax = (function(){
var cache = {};
return function(type, url, success){
//判断缓存是否存在,存在则直接返回
if(!!cache[url]) return cache[url];
//调用ajax函数,在请求结束时,将请求的数据先缓存起来
ajax(type, url, function(data){
cache[url] = data;
setTimeout(function(){ //设置该url的缓存时间为5分钟
cache[url] = undefined;
},300*1000);
//调用真正的回调函数
success(data);
});
}
})();
这就是代理模式,以上。
设计模式(单例)
这次我们讲的设计模式叫做 Singleton, 单例模式
那么单例模式是什么呢? 举个简单的例子
笔记本电脑-----》使用简单-----》结构复杂-----》禁止自私拆卸, 隐藏复杂细节,不希望用户修改内容。
代码:
var macbook = {
price : 9999,
.....
}
var macbook = (function(){
var price = 9999; //私有属性,不可更改
var power = "38.4v";
var memery = {};
return {
getPrice : function(){
return price;
}
}
})();
macbook对象是单一的,并且它包含了一些我们不可修改的属性
这样的封装确实隐藏了细节,同时变得更安全。
但是,这并不是标准的单例模式。
单例模式有个很重的定义:一个类只能创建一个实例,也就是说多次实例化将会得到相同的结果。
代码:
var MacBook = function(){
var price = 9999; //私有属性,不可更改
var power = "38.4v";
var memery = {};
this.getPrice = function(){
return price;
}
}
var macbook01 = new MacBook();
var macbook02 = new MacBook();
console.log(macbook01 == macbook02); //false
两次实例化对象并不相等,没有达到单例的效果
我们改造一下代码:
var MacBook = (function(){
var instance = null;
return function(){
var price = 9999;
this.getPrice = function(){
retrun price;
}
instance = this;
}
})();
//等价于我们得到了这样的构造函数
var MacBook = function(){
var price = 9999;
this.getPrice = function(){
return price;
}
instance = this;
}
//这样并不能保证是单例的,但我们只要加上一个判断
var MacBook = function(){
if(!!instance) return instance;
var price = 9999;
this.getPrice = function(){
return price;
}
instance = this;
}
实战练习: 设计一个函数,创建一个弹出窗口。窗口可以反复打开和关闭。
function Popup(info){
var div = document.createElement("div");
div.innerHTML = info;
document.body.appendChild(div);
//隐藏div
this.hide = function(){
div.style.display = "none";
}
}
分析:如果每次打开窗口都创建新的元素,是否浪费操作?
是否只需要一个DIV就可以反复使用? 那么可以用单例模式解决吗?
如何保证 new Popup("aaa") === new Popup("bbb"); //true
改造代码:
function Popup(info){
if(!!Popup.instance){
Popup.instance.show();
return;
}
//原有代码部分全部保留
this.show = function(){
div.style.display = "block";
}
Popup.instance = this;
}
单例模式的其他好处:跨作用域共享
function A(){
var c = new C();
c.prop = xxxx; //修改了c
}
function B(){
var c = new C(); //如何获取c,并得到最新prop属性
}
//单例
function C(){
if(!!C.ins) return C.ins;
//.....
C.ins = this;
}
设计模式(工厂)
这次我们讲的设计模式叫做 Factory, 工厂模式
那么工厂模式是什么呢?
所谓工厂,当然是生产内容的地方。 这里有一个重要的概念请大家记住: 工厂生产的,一般是一个完整的产品。
如果是一个半成品,需要我们后续加工,那工厂模式的意义就没有了。
举个简单的例子
手机工厂,生产的各种各样的手机,当你把产品拿到手以后,你可以直接使用
并不需要知道这台手机是如何生产的。手机工厂生产出来的手机在出厂时都是一模一样的。
我们玩
RedAlert(
红色警戒
)这款即时战略游戏时,一定会建造一个坦克工厂。
就像这样:
这个工厂里可以生产若干类型的坦克,比如幻影坦克,光棱坦克,爱国者导弹车等。
我们想得到一辆坦克,代码大概是这样的:
var tank01 = TankFactory.create("幻影");
var tank02 = TankFactory.create("光棱");
我们不需要知道坦克的创建过程。 坦克工厂的内部结构大概是这样:
var TankFactory = {
tanklist : {
"幻影" : function(){ //...... },
"光棱" : function(){ //...... }
},
create : function(type){
return new this.tanklist[type]();
}
};
当然,tanklist是不能随便让别人修改的,需要做成私有变量,于是改造一下代码
var TankFactory = (function(){
var tanklist = {
"幻影" : function(){ //...... },
"光棱" : function(){ //...... }
}
return {
create : function(type){
return new tanklist[type]();
}
};
})();
一个典型的工厂模式就是:
var oDiv = document.createElement("div");
var oSpan = document.createElement("span");
好处有两点:
第一,可以比较容易的批量化生产
第二,封装生产过程,让生产变得容易
当遇到一个构建起来比较复杂的对象时,就比较适合用工厂模式。
因此,往大了说,其实每个构造函数都是一个工厂。
例如:
var obj = new Object();
var now = new Date();
实际上,这些都是工厂,它们都可以批量生产,并且隐藏了复杂的构建细节。
实战应用:
著名的jQUeryUI框架中,有这样一个功能:
$("#dia").dialog();
如此简单的一句话,便可以生成一个经过美化的弹出层。
我们下面来自己写一个简单的DOM工厂。
var DomFactory = (function(){
var domlist = {
"LINK" : function(){
return document.createElement("a");
},
"BTN" : function(){
return document.createElement("button");
},
"IMG" : function(){
return document.createElement("img");
}
}
return {
create: function(type){
return new domlist[type]();
}
};
})();
//使用的时候像这样:
var link = DomFactory.create("LINK");
link.href = "http://www.xxxxxx";
不过这种写法可能跟平时用起来好像差别不大;
var link = document.createElement("a");
下面我们把这个工厂,改造一下,做一些复杂的事情。
var DomFactory = (function(){
var domlist = {
"LINKIMG" : function(href, src){
var link = document.createElement("a");
//去掉默认下划线
link.style["text-decoration"] = "none";
link.href = href;
var img = document.createElement("img");
img.src = src;
link.appendChild(img);
return link;
},
"LINKBTN" : function(href, text){
var link = document.createElement("a");
//去掉默认下划线
link.style["text-decoration"] = "none";
link.href = href;
var btn = document.createElement("button");
btn.innerHTML = text;
link.appendChild(btn);
return link;
}
}
return {
create: function(type){
return new domlist[type](...Array.from(arguments).slice(1));
}
};
})();
使用方法如下:
var linkbtn = DomFactory.create("LINKBTN", "http://www.china.com", "中国");
这样一来我们便得到了一个由超链接包裹着的按钮了。
这就是工厂方法带给我们的好处。
设计模式(适配器)
这次我们讲的设计模式叫做 Adapter, 适配器模式
所谓适配,就是将两个不兼容的东西变得兼容起来。
举如: 我们的笔记本电脑,接收的电压通常在30~50V之间。
可是国内的通用电压都是220V, 因此我们的笔记本电脑无法直接使用墙插上的电压
所以笔记本电源线就是一个适配器
通过
适配
再比如,苹果笔记是没有办法直接连接投影仪的。
因为苹果笔记本的接口是长这样的(下图),我们管它叫雷电接口
然而投影仪的接口长这样(下图),我们管它叫VGA接口
所以我们需要一个适配器(下图)
适配器还有一种应用场景,在平时也很常见。
比如你现在有这么一个需求, 你的好友喜得贵子,某天你要去探望,结果到了家门口发现忘了买礼物(随礼是中国人的陋习,不提倡)
不过好在身上有1000块,于是你就想干脆直接给钱吧,这样更实在。
可是! 难道直接这样给么?
这样太不雅观了,给人一种不礼貌不认真的感觉。主要是这样对方不好意思要。
于是你去超市,花1块钱买了个红包,就像这样
这下看起来好多了有没有?! 好了, 这个红包,就叫做适配器。它对我们的钱进行了包装,让对方可以欣然接受。
编程当中,我们更多的是采用这个红包的例子。
比如:
function IPHONE(){
this.phonecall = function(){
console.log("iphone打电话...");
};
this.playgame = function(){
console.log("iphone玩游戏...");
};
}
function IPAD(){
this.playgame = function(){
console.log("ipad玩游戏...");
};
}
var iphone = new IPHONE(); //生产了一台手机
var ipad = new IPAD(); //生产了一台平板
|
//在出厂前,要对产品进行测试。 注意,ipad只能玩游戏,不能打电话
//这是测试工具
function test(target){
try{
target.playgame();
} catch(e){
console.error("玩游戏功能测试失败!");
}
try{
target.phonecall();
} catch(e){
consol.error("打电话功能测试失败!");
}
}
|
//开始测试
test(iphone);
test(ipad);
结果发现,ipad在测试phonecall(打电话)功能时,出现失败!
因为ipad原本在设计的时候,就不具备通话功能,但是要想经过这道测试工序,我们就必须想点办法了
问题描述:
1,test要接受一个产品
2,我们手里有一个ipad
3,ipad不太符合test的需求,会导致不应该出现的错误
4,想办法在不修改ipad前提下,让ipad通过测试
这非常符合我们刚才所说的不兼容的场景, 于是,适配器模式要出现了!!
我们首先做一个适配器(红包),然后将ipad(钱)包装起来。
function Adapter(target){
this.playgame = function(){
if(!!target.playgame && typeof target.playgame === "function"){
target.playgame();
} else {
console.log("该产品目前不支持playgame功能");
}
}
this.phonecall = function(){
if(!!target.phonecall && typeof target.phonecall === "function"){
target.phonecall();
} else {
console.log("该产品目前不支持phonecall功能");
}
}
}
var ipad_apt = new Adapter(new IPAD());
test(ipad_apt);
//测试通过!
|
实战练习:
Math.min方法用来求最小值,但很可惜它只接收2个参数,多余参数会被忽略
于是我们做个适配器包装一下,让它可以接收任意多个参数
var MathAdapter = {
min : function(){
Math.min.apply(window.Math, arguments);
}
}
Math.min(5,4,3,2,1); //返回4
MathAdapter.min(5,4,3,2,1); //返回1
|
设计模式(责任链)
这次我们讲的设计模式叫做Chain of responsbility, 责任链模式
所谓责任链,即是把责任拆分到多个人身上,然后各自履行各自的责任即可
举个例子:
我们要生产一件衣服,大概需要四个工序: 织布、设计、剪裁、缝合
那么接下来,我需要将准备好的原材料交给张三。
张三擅长织布,但不懂得其他工序,不过没关系,我告诉张三,他只管织布,然后把织好的布料交给李四就行,剩下的就不用管了。
李四是设计师,但不会裁缝,不过没关系,我告诉李四,他只需要根据布料款式设计就好,然后把设计图交给王五就行。
王五擅长测量剪裁,他把衣服剪裁好以后,就交给赵六
赵六最后通过缝合制作成衣。
你看,在这个过程中,每个人的职责都更单一了,大家分工协作,只专注做好自己分内的事情
假设我们有一系列处理字符串的工具,我们希望按照需求来定制我们的处理器。
目前有 filterA、filterB、filterC三个处理器
我们希望每个过滤器职责单一,只负责处理一种功能
|
JS代码
|
function
filterA
(
str
){
str
=
str.toLowerCase()
return
filterB(str);
//过滤器A完成任务,把剩下的职责交给B
}
function
filterB
(
str
){
str[
0
]
=
str[
0
].toUpperCase();
return
filterC(str);
//过滤器B完成任务,把剩下的职责交给C
}
function
filterC
(
str
){
str
=
str.substring(
0
,
50
)
+
"..."
;
return
str;
}
|
上面的代码,做到了职责单一的功能,并且大家形成一个链条,按顺序执行。
但是!这样做代码的耦合性太强了,A调用了B,B调用了C,代码之间产生了联系,就意味他们不是独立的,或者说没有办法独立存在!
于是我们把过滤器改造成独立功能,消除耦合
|
function
filterA
(
str
){
//全体字母变小写
return
str
;
}
function
filterB
(
str
){
//首字母变大写
return
str
;
}
function
filterC
(
str
){
//超过50个字符后,用...替换
return
str
;
}
var
filterChain
=
{
chain : [filterA, filterB, filterC],
//这里可以设定我们需要的过滤器
doHandle :
function
(
data
,
index
){
//已经超出边界,则链条遍历完毕,返回
if
(index
==
chain.length)
return
;
//如果是第一次调用,则index为0
index
==
undefined
?
index
=
0
:
"donothing"
;
//取出责任链中第一个处理函数,处理并返回数据
var
resdata
=
this
.
chain[index](data);
//将处理后的数据返回给下一个
this
.doHandle(resdata, index
+
1
);
}
};
filterChain.doHandle(str);
|