JavaScript 设计模式学习与实践(二)

我们接着上一篇博客继续总结和学习JavaScript的常用设计模式,这一次我们总结两个在JavaScript角度来讲比较像的模式,也就是代理模式,策略模式。

这两个模式在非函数式编程编程的语言中,如Java中区别是比较明显的,但是用JavaScript来看则是比较相似的。

策略模式

策略模式定义

在程序设计中,我们常常遇到这种情况,要实现某一个功能有多种方案可供选择。比如一个压缩文件的程序,既可以选择zip算法,也可以选择gzip算法

这些算法灵活多样,而且可以随意互相替。这种解决方案就是策略模式。

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使他们可以相互替换

使用策略模式计算奖金

比如我们现在有个需求,年底发放的奖金是根据绩效决定的,绩效S的是4个月的工资,A的3个月,B的2个月

基础代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var calculateBonus = function(performanceLevel, salary) {
if (performanceLevel === 'S') {
return salary * 4;
}

if (performanceLevel === 'A') {
return salary * 3
}

if (performanceLevel === 'B') {
return salary * 2
}
}

这段代码十分简单,但是也存在很多的缺点:

  • calculateBonus函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有分支
  • calculateBonus函数缺乏弹性,如果增加一种新的绩效等级C,或者想把绩效系数改为5,我们必须深入calculateBonus函数内部实现,这违反开闭原则
  • 算法的复用性差,如果在程序的其他地方复用其中部分奖金的算法,只有复制粘贴

使用策略模式重构代码

将不变的部分和变化的部分分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的是将算法的使用与算法的实现分离开来

在我们的这个例子中,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是变化的,不同的绩效对应着不同的的计算规则

一个基于策略模式的程序至少有两部分组成,第一部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二部分是环境类Context,Context接受客户的请求,随后把请求委托给一个具体的策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var performanceS = function() {}

performanceS.prototype.calculate = function(salary) {
return salary * 4;
}

var performanceA = function() {}

performanceA.prototype.calculate = function(salary) {
return salary * 3;
}

var performanceB = function() {}

performanceB.prototype.calculate = function(salary) {
return salary * 2;
}

var Bonus = function() {
this.salary = null;
this.strategy = null;
}

Bonus.prototype.setSalary = function(salary) {
this.salary = salary;
}

Bonus.prototype.setStrategy = function(strategy) {
this.salary = strategy;
}

Bonus.prototype.getBonus = function() {
return this.strategy.calculate(this.salary);
}

在客户对Context发起请求时,Context总是把请求委托给这些策略对象中的一种。

JavaScript版本的策略模式

我们一开始就说过,JavaScript的函数式编程会让某些设计模式具体实现变得不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var strategies = {
'S': function(salary) {
return salary * 4;
},
'A': function(salary) {
return salary * 3;
},
'B': function(salary) {
return salary * 2
}
}

var calculateBonus = function (level, salary) {
return strategies[level](salary);
}

代理模式

代理模式定义

代理模式是为一个对象提供一个代用品或占位符,以便控制对他的访问。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求作出一些处理后,再把请求转交给对象本身。

当策略模式只有一个策略的时候,代理模式和策略模式看起来比较像,主要区别是,代理模式是代理对目标对象的访问,而策略模式则是执行不同的策略。

使用代理模式来送花

假设小明想要送花给A,我们用代码来模拟这个过程就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Flower = function() {}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
}

var A = {
reveiveFlower: function(flower) {
consoloe.log(`收到${flower}`)
}
}

xiaoming.sendFlower(A);

我们引入B来帮我们送花的话,就是代理模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Flower = function() {}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
}

var B = {
reveiveFlower: function(flower) {
A.receiveFlower(flower)
}
}

var A = {
reveiveFlower: function(flower) {
consoloe.log(`收到${flower}`)
}
}

xiaoming.sendFlower(B);

但是这段代码看起来好像除了绕了一圈,并没有什么实际用处。

当然,也确实如此,不过如果我们再加一个需求就是,我们需要A心情好的时候再送花,而只有B知道A什么时候心情好,这个代理模式就有用处了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var Flower = function() {}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
}

var B = {
reveiveFlower: function(flower) {
A.listenGoodMoon(function() {
A.receiveFlower(flower)
});
}
}

var A = {
reveiveFlower: function(flower) {
consoloe.log(`收到${flower}`)
},
listenGoodMood: function(callback) {
callback();
}
}

xiaoming.sendFlower(B);

保护代理和虚拟代理

上面的例子虽然简单,但是我们可以看到两种代理的身影。代理B可以帮助代理A过滤到一些请求,这种就叫做保护代理。

而如果new Flower是一个代价比较大的操作,我们可以将new Flower的操作交给B去做,从而节省开销,这种就叫做虚拟代理。

1
2
3
4
5
6
7
8
var B = {
reveiveFlower: function() {
A.listenGoodMoon(function() {
var flower = new Flower();
A.receiveFlower(flower);
});
}
}

虚拟代理实现图片预加载

在web开发中,如果给某个img标签节点设置src属性,如果图片过大或者网络不佳,图片的位置往往有一段时间是空白的。

常见的做法是先用一张loading图占位,然后异步的方式去加载图片,图片加载好以后再填充到img中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var myImage = {
setSrc: function(src) {
var imageNode = document.createElement('img');
document.body.appendChild(imageNode);
imageNode.src = src;
}
}

var proxyImage = (function() {
var img = new Image();
img.onload = function() {
myImage.setSrc(this.src)
}

return {
setSrc: function(src) {
myImage.setSrc('file://loading.png');
img.src = src;
}
}
})()

proxyImage.setSrc('http://imgcache.com/music/aaa.jpg');

代理模式的意义

我们也许会疑惑,不过是实现一个预加载的功能,即使不需要引入任何模式也能做,那么引入代理模式的好处在哪里呢?我们看一下不用代理模式来做图片预加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var MyImage = (function() {
var imageNode = document.createElement('img');
document.body.appendChild(imageNode);

var img = new Image();
img.onload = function() {
imageNode.setSrc(this.src)
}

return {
setSrc: function(src) {
imageNode.src = 'file://loading.png';
img.src = src;
}
}
})()

为了说明代理模式的意义,我们引入一个面向对象的设计的原则————单一职责原则。

单一职责原则指的是,就一个类(通常也包含对象和函数)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可以有多个。

面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

职责被定义为“引起变化的原因”。上段代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。

代理和本体接口的一致性

如果我们有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中的关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接收请求的过程对用户来说是透明的。

在Java等语言中,代理和本体都需要显式实现同一个接口,一方面接口保证了它们有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型。

值得一提的是,如果代理本体和对象都为一个函数,函数必然能被执行,则可以认为它们也有一致的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myImage = function(src) {
var imageNode = document.createElement('img');
document.body.appendChild(imageNode);
imageNode.src = src;
}

var proxyImage = (function() {
var img = new Image();
img.onload = function() {
myImage.setSrc(this.src)
}

return function(src) {
myImage.setSrc('file://loading.png');
img.src = src;
}
})()

proxyImage('http://imgcache.com/music/aaa.jpg');

其他的代理模式

其实我们平时开发用了很多的代理模式,比如我们常用的防抖和节流,其实就是一种代理,将多个请求合并为一次处理。

再比如我们的缓存和缓冲代理,把我们对数据库的直接访问变为对缓冲区的访问,如果缓冲区没有,那么缓冲区再去数据库查询。

再比如我们平时常用的消息队列,其实也可以是代理模式的一种思想。