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( '发射原子弹' );
}

// 最后看看测试结果
var plane = new Plane();
plane = new MissileDecorator( plane ); 
plane = new AtomDecorator( plane );
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire 方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的 下一个对象。 
因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象

二、装饰者也是包装器

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

三、回到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();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹

四、用AOP装饰函数

Function.prototype.before = function( beforefn ){
  // 保存原函数的引用
  var __self = this;
  // 返回包含了原函数和新函数的"代理"函数
  return function(){ 
    // 执行新函数,且保证this不被劫持,新函数接受的参数 
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
    beforefn.apply( this, arguments ); 
    return __self.apply( this, arguments ); // 执行原函数并返回原函数的执行结果, // 并且保证this不被劫持
  } 
}
Function.prototype.after = function( afterfn ){ 
  var __self = this;
  return function(){
    var ret = __self.apply( this, arguments ); 
    afterfn.apply( this, arguments );
    return ret;
  } 
};
Function.prototype.before 接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。接下来把当前的 this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。
// 实例1:
document.getElementById = document.getElementById.before(function() {
  alert (1);
});
var button = document.getElementById('button');
console.log(button)

// 实例2(改造window.onload):
window.onload = function(){ 
  alert (11);
}
window.onload = ( window.onload || function(){} ).before(function(){ 
  alert (22);
}).after(function(){ 
  alert (33);
}).after(function(){ 
  alert (44);
});
值得提到的是,上面的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)
  }, function(){
    alert (2)
  }
);
a = a();

五、AOP的应用实例

不论是业务代码的编写,还是在框架层面,我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高复用性的系统。分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应用之一。

5.1:假设现在我们需要点击一个按钮后既要负责打开登录浮层,和负责数据上报,我们看到在showLogin函数里,这是两个层面的功能,在此处却被耦合在一个函数里。

{/* <button tag="login" id="button">点击打开登录浮层</button> */}
var showLogin = function(){ 
  console.log( '打开登录浮层' ); 
  log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
  console.log( '上报标签为: ' + tag );
  // (new Image).src = 'http:// xxx.com/report?tag=' + tag;   // 真正的上报代码略
}
document.getElementById( 'button' ).onclick = showLogin;

// 使用 AOP分离之后,代码如下:
var showLogin = function(){ 
  console.log( '打开登录浮层' );
}
var log = function(){
  console.log( '上报标签为: ' + this.getAttribute( 'tag' ) );
}
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;

5.2:插件式的表单验证

<body>
  用户名:<input id="username" type="text"/>
  密码: <input id="password" type="password"/>
  <input id="submitBtn" type="button" value="提交"> 
</body>
传统的表单验证的写法:formSubmit 函数在此处承担了两个职责,除了提交 ajax 请求之外,还要验证用户输入的合法性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性。
var username = document.getElementById( 'username' ),
    password = document.getElementById( 'password' ), 
    submitBtn = document.getElementById( 'submitBtn' );
var formSubmit = function(){
  if ( username.value === '' ){
    return alert ( '用户名不能为空' ); 
  }
  if ( password.value === '' ){
    return alert ( '密码不能为空' );
  }
  var param = {
    username: username.value, 
    password: password.value
  }
  ajax( 'http:// xxx.com/login', param );
}
submitBtn.onclick = function(){ 
  formSubmit();
}
改造之后的写法,在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系, formSubmit = formSubmit.before( validata )这句代码,如同把校验规则动态接在 formSubmit 函数 之前,validata 成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分 开维护这两个函数。再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式, 用在不同的项目当中。
Function.prototype.before = function( beforefn ){
  var __self = this; 
  return function(){
    if ( beforefn.apply( this, arguments ) === false ){
    // beforefn 返回 false 的情况直接 return,不再执行后面的原函数 return;
    }
    return __self.apply( this, arguments ); 
  }
}
var validata = function(){
  if ( username.value === '' ){
    alert ( '用户名不能为空' );
    return false; 
  }
  if ( password.value === '' ){ 
    alert ( '密码不能为空' ); 
    return false;
  } 
}
var formSubmit = function(){ 
  var param = {
    username: username.value,
    password: password.value
  }
  ajax( 'http:// xxx.com/login', param ); 
}
formSubmit = formSubmit.before( validata );
submitBtn.onclick = function(){ 
  formSubmit();
}

六:装饰者模式和代理模式

  装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供 一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送 请求。
  代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直 接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代 理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对 象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系 可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确 定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条 长长的装饰链。
posted @ 2019-11-14 17:25  雨水一盒~  阅读(218)  评论(0编辑  收藏  举报