【学习笔记javascript设计模式与开发实践(装饰者模式)----15】

第15章 装饰者模式

15.1 模拟传统面向对象语言的装饰者模式

 

首先要提出来的是,作为一门解释执行语言,给javascript中的对象动态添加或者改变职责是一件再简单不过的事情,虽然这种做法改动了对象自身,跟传统定义中的装饰者模式并不一样,但这无疑更符合javascript的语言特色:

var obj = {
name:’sven’,
address:’深圳市’
}
obj.address = obj.address+’福田区’;

传统面向对象语言中的装饰者模式在javascript中适用的场景并不多,如上面的代码所示,通常我们并不太介意改动对象自身。尽管如此,本节我们还是稍微模拟一下传统面向对象语言中的装饰者模式实现。

假设我们在编写一个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射,升到第三级时可以发射原子弹。原始飞机类:

var Plane = function(){}
Plane.prototype.fire= function(){
  console.log('发射普通子弹');
}
 
接下来增加两个装饰类,分别是导弹和原子弹
var MissileDecorator= function(plane){
    this.plane= plane;
}

MissileDecorator.prototype.fire= function(){
   this.plane.fire();
    console.log('发射导弹');
}

var AtomDecorator = function(plane){
   this.plane= plane;
}

AtomDecorator.prototype.fire= function(){
   this.plane.fire();
    console.log('发射原子弹');
}

导弹类和原子弹类的构造函数都接受参数plane对象,并且保存好这个参数,在它们的fire方法中,除了执行自身的操作之外,还调用plane对象的fire方法。

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,gadng个聚合对象。这些对象都拥有相同的接口(fire)当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中下一个对象。

因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。

var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire(); //分别输出:发射普通子弹、发射导弹、发射原子弹

15.2 装饰者也是包装器

在《设计模式》成书之前,GoF原想把装饰者模式称为包装器模式。

从功能上而言,decorator能很好地描述这个模式,但从结构上看,wrapper的说法更加贴切。装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有对象,每个对象都有处理这条请求的机会。

15.3 回到javascript的装饰者

javascript语言动态改变对象相当容易,我们可以直接改写对象或对象的某个方法,并不需要使用“类”来实现装饰者模式如下:

var plane= {
    fire:function(){
       console.log('发射普通子弹');
    }
}

var missileDecorator = function(){
    console.log("发射导弹");
}

var atomDecorator = function(){
    console.log("发射原子弹");
}

var fire1= plane.fire;

plane.fire= function(){
    fire1();
    missileDecorator();
}
var fire2= plane.fire;
plane.fire= function(){
     fire2();
     atomDecorator();
}
 
plane.fire();//分别输出:发射普通子弹、发射导弹、发射原子弹

15.4 装饰函数

在javascript中,几乎一切都是对象,其中函数又被称为一等对象。在平时的开发工作中,也许大部分时间都在和函数打交道。在javascript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能,在代码的运行期间我们很难切入某个函数的执行环境。

要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放—封闭原则。

var a = function(){
  alert(1);
}
//改成
var a = function(){
 alert(1);
 alert(2);
}

很多时候我们不想去碰原函数,也许原函数是由其他同事编写的,里面的实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿意碰触的阴暗角落里。现在需要一个办法,在不改变函数源代码的情况下,给函数增加功能,这正是开发—封闭原则给我们指出的光明道路。

其实我们在上一节中,我们已经找到一种答案,通过保存原引用方式就可以改写某个函数:

var a = function(){
  alert(1);
}
var _a = a;
a = function(){
_a();
alert(2);
}
a();

这是在实际开发中很常见的一种做法,比如我们想给window绑定onload事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖之前的window.onload函数的行为,我们一般都会先保存好原先的window.onload,把它放入新的window.onload里执行:

window.onload = function(){
 alert(1);
}
var _onload = window.onload||function(){};
window.onload = function(){
 _onload();
 alert(2);
}

这样的代码当然是符合开放—封闭原则的,我们在增加新功能的时候,确实没有修改原window.onload代码,但是这种方式存在以下两个问题:

o  必须维护_onload这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。

o  其实还遇到了this被劫持的问题,在window.onload的例子中没有这个烦恼,是因为调用普通函数_onload时,this也指向window,跟调用window.onload时一样。现在把window.onload换成document.getElementById,代码如下:

var_getElementById = document.getElementById;
document.getElementById= function(id){
   alert(1);
   return _ getElementById(id); //会抛出异常的
}

发生异常处的_ getElementById是一个全局函数,当调用一个全局函数时,this是指向window的,而document.getElementById方法的内部实现需要使用this引用,this在这个方法内预期是指向document,而不是window,这是错误发生的原因,所以使用现在的方式给函数增加功能并不保险。

改进后的代码可以满足需求,我们要手动把document作为上下文this传入_ getElementById:

var _getElementById = document.getElementById;
document.getElementById= function(){
   alert(1);
   return _ getElementById.apply(document,arguments);//会抛出异常的
}

15.5 用AOP装饰函数

首先给出Function.prototype.before方法和Function.prototype.after方法:

Function.prototype.before =function(beforefn){
  var __self = this;
  return function(){
beforefn.apply(this,arguments);
return__self.apply(this,arguments);
   }
}
 
Function.prototype.after =function(afterfn){
  var __self = this;
  return function(){
var ret =__self.apply(this,arguments);
afterfn.apply(this,arguments);
return ret;
   }
}

再回到window.onload的例子,看看Function.prototype.before来增加新的window.onload事件是多么简单:

window.onload = function(){
   alert(1);
}
window.onload =(window.onload||function(){}).after(function(){
  alert(2);
}).after(function(){
  alert(3);
}).after(function(){
  alert(4);
});

值得提到的是,上面的AOP实现是在Function.prototype上添加before和after方法,但许多人不喜欢这种污染原型的方式,那么我们可以做一些变通,把原函数和新函数都作为参数传入before,after方法

var before = function(fn,beforefn){
    return function(){
       beforefn.apply(this,arguments);
       return fn.apply(this,arguments);
    }
}

var a = before(function(){alert(3),alert(2)});
a = before(a,function(){alert(1)});
a();

15.6 AOP的应用实例

用AOP装饰函数的技巧在实际开发中非常有用。不论是业务代码编写,还是在框架层我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高利用性的系统。

15.6.1 数据统计上报

分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应该之一。在项目开发的结尾阶段难免要加上很多统计数据的代码,这些过程可能让我们被迫改动早已封装好的函数。

比如页面中有一个登录button,点击这个button会弹出登录浮层,与此同时要进行数据上报来统计有多少用户点击了这个登录button。 

<html>
<body>
   <button tag=”login” id = “button”>点击打开登录浮层</button>
   <script>
          var showLogin = function(){
              console.log(‘打开登录浮层’);
              log(this.getAttribute(‘tag’));
          }
          var log = function(tag){
              console.log(‘上报标签为:’+tag);
              //(new Image).src = ‘http://xxxxx…’+tag
          }
   </script>
</body>
</html>

我们看到在showLogin函数里,即要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处被耦合在一个函数里,使用AOP分离之后如下:

Function.prototype.after =function(afterfn){
  var __self = this;
  return function(){
     var ret =__self.apply(this,arguments);
         afterfn.apply(this,arguments);
         return ret;
   }
}
 
var showLogin = function(){
   console.log(‘打开登录浮层’);
}
var log = function(){
  console.log(‘上报标签为:’+this.getAttribute(‘tag’);
}
showLogin = showLogin.after(log); //打开登录浮层之后上报数据

15.6.2 用AOP动态改变函数的参数

观察Function.prototype.before方法

Function.prototype.before =function(beforefn){
  var __self = this;
  return function(){
beforefn.apply(this,arguments);//(1)
return__self.apply(this,arguments); //(2)
   }
}

从这段代码的(1)处和(2)处可以看到,beforefn和原函数__self共用一组参数列表arguments,当我们在beforefn的函数体内改变arguments的时候,原函数__self接收的参数列表自然也会变化。

下面的例子展示了如何通过Fuction.prototype.before方法给函数func的参数param动态添加属性b:

var func = function(param){
  console.log(param);
}
func = func.before(function(param){
  param.b = ‘b’;
});
func({a:’a’}); //{a:’a’,b:’b’}

现在有一个用于发起ajax请求的函数,这个函数负责项目中所有的ajax异步请求:

var ajax = function(type,url,param){
console.dir(param);
//ajax略
}
ajax(‘get’,’http://xxx.com/userinfo’,{name:’sven’});

上面的代码表示向后台cgi发起一个请求来获取用户信息,传递给cgi的参数是{name:’sven’}。

ajax函数在项目中一直运转良好,跟cgi的合作也很愉快。直到有一天,我们的网站遭受了CSRF攻击。解决CSRF攻击最简单的一个办法就是在http请求中带上一个Token对象。

假设我们已经有一个用于生成Token的函数

var getToken = function(){
  return ‘Token’;
}

现在的任务就是给每个ajax请求都加上Token参数

var ajax = function(){
   param = param||{};
   param.Token =getToken();
}

虽然已经解决问题,但我们的ajax函数相对变得僵硬了,每个从ajax函数里发出的请求都自动带上了Token参数,虽然在现在的项目中没有什么问题,但如果将业把这个函数移植到其他项目上,或者把它放到一个开源库中供其他人使用,Token参数都将是多余的。

也许另一个项目不需要验证Token,或者是Token的生成方式不同,无论是哪种情况,都必须重新修改ajax函数。

我们还原ajax函数

var ajax = function(type,url,param){
  console.log(param);
}

然后所Token参数通过Function.prototype.before装饰到ajax函数的参数param对象中:

ajax =ajax.before(function(type,url,param){
  param.Token = getToken();
}); 
ajax(‘get’,’http://xx.com/usrinfo’,{name:’sven’});

15.6.3 插件式的表单验证 

var username= document.getElementById('username');
var password= document.getElementById('password');
var submitbtn= document.getElementById('submitbtn');
var formSubmit = function(){
    if(username.value===''){
       return alert('用户名不能为空');
    }
    if(password.value===''){
       return alert('密码不能为空')
    }
    var param= {
       username:username,
       password:password
    }
    ajax('http://xxx.login',param);
}
submitbtn.onclick = function(){
    formSubmit();
}
//本节的目的是分离校验输入和提交ajax请求的代码,我们把校验输入的逻辑放到validate函数中并约定validate函数返回false的时候,表壳校验未通过
var validata= function(){
    if(username.value===''){
        return alert('用户名不能为空');
        return false;
    }
    if(password.value===''){
        return alert('密码不能为空')
        return false;
    }
}
 
var formSubmit = function(){
  if(validate()===false){
    return false;
  }   
  var param= {
       username:username,
       password:password
    }
    ajax('http://xxx.login',param);
}
//再进一步优化
var username= document.getElementById('username');
var password= document.getElementById('password');
var submitbtn= document.getElementById('submitbtn');
var validata = function(){
    if(username.value===''){
        return alert('用户名不能为空');
        return false;
    }
    if(password.value===''){
        return alert('密码不能为空')
        return false;
    }
}
var formSubmit = function(){
   
    var param= {
       username:username,
       password:password
    }
    ajax('http://xxx.login',param);
}
formSubmit = formSubmit.before(validata);
submitbtn.onclick = function(){
    formSubmit();
}

如同把检验规则动态接在formSubmit函数之前,validata成为一个即插即用的函数,它甚至可能被写成配置文件的形式,这有利于我们分开维护这两个函数。 

15.7 装饰者模式和代理模式

代理模式和装饰者模式的最重要区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问。或者在访问本体之前做一些额外的事情。装饰者模式的作用就为对象动态加入行为。换名话说,代理模式强调一种关系,这种关系可以静态表达,也就是说,这种关系在一天始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时,代理模式只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值