我们接着上一篇博客继续总结和学习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');
|
其他的代理模式
其实我们平时开发用了很多的代理模式,比如我们常用的防抖和节流,其实就是一种代理,将多个请求合并为一次处理。
再比如我们的缓存和缓冲代理,把我们对数据库的直接访问变为对缓冲区的访问,如果缓冲区没有,那么缓冲区再去数据库查询。
再比如我们平时常用的消息队列,其实也可以是代理模式的一种思想。