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