前面的话

  在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么可以使用装饰者模式。装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。本文将详细介绍装饰者模式

概念

  在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性

  使用继承还会带来另外一个问题,在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长。比如现在有4种型号的自行车,为每种自行车都定义了一个单独的类。现在要给每种自行车都装上前灯、尾灯和铃铛这3种配件。如果使用继承的方式来给每种自行车创建子类,则需要4×3=12个子类。但是如果把前灯、尾灯、铃铛这些对象动态组合到自行车上面,则只需要额外增加3个类

  这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓

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

var obj ={
name:'match',
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方法。这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象

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

  在《设计模式》成书之前,GoF原想把装饰者(decorator)模式称为包装器(wrapper)模式。从功能上而言,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();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹

装饰函数

  在javascript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,很难切入某个函数的执行环境。要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放——封闭原则

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

  很多时候不想去碰原函数,也许原函数是由其他同事编写的,里面的实现非常杂乱。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,通过保存原引用的方式就可以改写某个函数:

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

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

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

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

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

  2、遇到了this被劫持的问题,在window.onload的例子中没有这个烦恼,是因为调用普通函数_onload时,this也指向window,跟调用window.onload时一样(函数作为对象的方法被调用时,this指向该对象,所以此处this也只指向window)。现在把window.onload换成document.getElementById,代码如下:

var _getElementById = document.getElementById;
document.getElementById= function(id){
alert();
return _getElementById(id); //(1)
}
var button = document.getElementById('button');

  执行这段代码,看到在弹出alert(1)之后,紧接着控制台抛出了异常:

//输出:Uncaught TypeError:Illegal invocation

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

  改进后的代码可以满足需求,要手动把document当作上下文this传入_getElementById:

<button id="button"></button>
<script>
var _getElementById = document.getElementById;
document.getElementById=function(){
alert();
return _getElementById.apply(document,arguments);
}
var button = document.getElementById('button');
</script>

  但这样做显然很不方便

AOP

  下面使用AOP来提供一种完美的方法给函数动态增加功能

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

Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,且保证this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
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指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。通过Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持。Function.prototype.after的原理跟Function.prototype.before一模一样,唯一不同的地方在于让新添加的函数在原函数执行之后再执行

  下面是一个例子

<button id="button"></button>
<script>
Function.prototype.before = function( beforefn ){
var __self = this;
return function(){
beforefn.apply( this, arguments );
return __self.apply( this, arguments );
}
}
document.getElementById = document.getElementById.before(function(){
alert ();
});
var button = document.getElementById( 'button' );
console.log( button );
</script>

  再回到window.onload的例子,用Function.prototype.before来增加新的window.onload事件非常简单

window.onload = function(){
alert ();
}
window.onload = ( window.onload || function(){} ).after(function(){
alert ();
}).after(function(){
alert ();
}).after(function(){
alert ();
});

  值得提到的是,上面的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 ()},
function(){alert ()}
);
a = before( a, function(){alert ();} );
a();

AOP应用实例

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

【数据统计上报】

  分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应用之一。在项目开发的结尾阶段难免要加上很多统计数据的代码,这些过程可能让我们被迫改动早已封装好的函数。比如页面中有一个登录button,点击这个button会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button

<html>
<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://xx.com/report?tag=' + tag; // 真正的上报代码略
}
document.getElementById( 'button' ).onclick = showLogin;
</script>
</html>

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

<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
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 ); // 打开登录浮层之后上报数据
document.getElementById( 'button' ).onclick = showLogin;
</script>
</html>

【用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接收的参数列表自然也会变化

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

var func = function(param){
console.log(param); //输出:{a:"a",b:"b"}
} func = func.before(
function(param){
param.b='b';
}); func({a:'a'});

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

var ajax =f unction(type,url,param){
console.dir(param);
//发送ajax请求的代码略
};
ajax('get','http://xx.com/userinfo',{name:'match'});

  上面的伪代码表示向后台cgi发起一个请求来获取用户信息,传递给cgi的参数是{name:'match'}。ajax函数在项目中一直运转良好,跟cgi的合作也很愉快。直到有一天,网站遭受了CSRF攻击。解决CSRF攻击最简单的一个办法就是在HTTP请求中带上一个Token参数。假设已经有一个用于生成Token的函数:

var getToken = function(){
return'Token';
}

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

var ajax = function(type,url,param){
param=param||{};
Param.Token=getToken(); //发送ajax请求的代码略...
};

  虽然已经解决了问题,但ajax函数相对变得僵硬了,每个从ajax函数里发出的请求都自动带上了Token参数,虽然在现在的项目中没有什么问题,但如果将来把这个函数移植到其他项目上,或者把它放到一个开源库中供其他人使用,Token参数都将是多余的。也许另一个项目不需要验证Token,或者是Token的生成方式不同,无论是哪种情况,都必须重新修改ajax函数

  为了解决这个问题,先把ajax函数还原成一个干净的函数:

var ajax = function(type,url,param){
console.log(param); //发送ajax请求的代码略
};

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

var getToken =function(){
return'Token';
}
ajax=ajax.before(function(type,url,param){
param.Token=getToken();
}); ajax('get','http://xx.com/userinfo',{name:'match'});

  从ajax函数打印的log可以看到,Token参数已经被附加到了ajax请求的参数中:

{name:"match",Token:"Token"}

  明显可以看到,用AOP的方式给ajax函数动态装饰上Token参数,保证了ajax函数是一个相对纯净的函数,提高了ajax函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改

【插件式表单验证】

  在一个Web项目中,可能存在非常多的表单,如注册、登录、修改用户信息等。在表单数据提交给后台之前,常常要做一些校验,比如登录的时候需要验证用户名和密码是否为空,代码如下:

<body>
用户名:<input id="username" type="text"/>
密码: <input id="password" type="password"/>
<input id="submitBtn" type="button" value="提交"></button>
<script>
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://xx.com/login', param ); // ajax 具体实现略
}
submitBtn.onclick = function(){
formSubmit();
}
</script>
</body>

  formSubmit函数在此处承担了两个职责,除了提交ajax请求之外,还要验证用户输入的合法性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性。下面来分离校验输入和提交ajax请求的代码,把校验输入的逻辑放到validata函数中,并且约定当validata函数返回false的时候,表示校验未通过,代码如下:

var validata = function(){
if ( username.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( password.value === '' ){
alert ( '密码不能为空' );
return false;
}
} var formSubmit = function(){
if ( validata() === false ){ // 校验未通过
return;
}
var param = {
username: username.value,
password: password.value
}
ajax( 'http:// xxx.com/login', param );
} submitBtn.onclick = function(){
formSubmit();
}

  现在的代码已经有了一些改进,把校验的逻辑都放到了validata函数中,但formSubmit函数的内部还要计算validata函数的返回值,因为返回值的结果表明了是否通过校验。接下来进一步优化这段代码,使validata和formSubmit完全分离开来。首先要改写Function.prototype.before,如果beforefn的执行结果返回false,表示不再执行后面的原函数,代码如下:

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://xx.com/login', param );
} formSubmit = formSubmit.before( validata ); submitBtn.onclick = function(){
formSubmit();
}

  在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系,formSubmit=formSubmit.before(validata)这句代码,如同把校验规则动态接在formSubmit函数之前,validata成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于分开维护这两个函数。再利用策略模式稍加改造,就可以把这些校验规则都写成插件的形式,用在不同的项目当中

  值得注意的是,因为函数通过Function.prototype.before或者Function.prototype.after被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。代码如下:

var func = function(){
alert();
}
func.a='a';
func=func.after(function(){
alert();
});
alert(func.a); //输出:undefined

  另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响

装饰者模式和代理模式

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

javascript设计模式——装饰者模式的更多相关文章

  1. 从ES6重新认识JavaScript设计模式: 装饰器模式

    1 什么是装饰器模式 向一个现有的对象添加新的功能,同时又不改变其结构的设计模式被称为装饰器模式(Decorator Pattern),它是作为现有的类的一个包装(Wrapper). 可以将装饰器理解 ...

  2. JavaScript设计模式----装饰者模式

    装饰者模式的定义: 装饰者(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责.与继承相比,装饰者是一种更轻便灵活的做法. 装饰者模式的特点: 可以动态的给某个对 ...

  3. 读书笔记之 - javascript 设计模式 - 装饰者模式

    本章讨论的是一种为对象增添特性的技术,它并不使用创建新子类这种手段. 装饰者模式可以透明地把对象包装在具有同样接口的另一对象之中,这样一来,你可以给一些方法添加一些行为,然后将方法调用传递给原始对象. ...

  4. JavaScript设计模式—装饰器模式

    装饰器模式介绍 为对象添加新的功能,不改变其原有的结构和功能,原有的功能还是可以使用,跟适配器模式不一样,适配器模式原有的已经不能使用了,装饰器示例比如手机壳 UML类图和代码示例 Circle示原来 ...

  5. JavaScript设计模式之----组合模式

    javascript设计模式之组合模式 介绍 组合模式是一种专门为创建Web上的动态用户界面而量身制定的模式.使用这种模式可以用一条命令在多个对象上激发复杂的或递归的行为.这可以简化粘合性代码,使其更 ...

  6. Java设计模式——装饰者模式

    JAVA 设计模式 装饰者模式 用途 装饰者模式 (Decorator) 动态地给一个对象添加一些额外的职责.就增加功能来说,Decorator 模式相比生成子类更为灵活. 装饰者模式是一种结构式模式 ...

  7. JAVA设计模式--装饰器模式

    装饰器模式 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构.这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装. 这种模式创建了一个装饰 ...

  8. javascript的装饰者模式Decorator

    刚开始看这段代码有点绕,现在回过头来看,so easy! Function.prototype.before = function(beforefn){ var _self = this; retur ...

  9. 从源码角度理解Java设计模式——装饰者模式

    一.饰器者模式介绍 装饰者模式定义:在不改变原有对象的基础上附加功能,相比生成子类更灵活. 适用场景:动态的给一个对象添加或者撤销功能. 优点:可以不改变原有对象的情况下动态扩展功能,可以使扩展的多个 ...

随机推荐

  1. 关于C#开发中那些编码问题

    最近一直在搞各种编码问题,略有心得,与大家分享一番. System.Text提供了Encoding的抽象类,这个类提供字符串编码的方法.常用的编码方式主要有ASCII,Unicode,UTF8(Uni ...

  2. Problem L

    Problem Description 在2×n的一个长方形方格中,用一个1× 2的骨牌铺满方格,输入n ,输出铺放方案的总数. 例如n=3时,为2× 3方格,骨牌的铺放方案有三种,如下图: L&qu ...

  3. 2015上海赛区B Binary Tree

    B - Binary Tree   Description The Old Frog King lives on the root of an infinite tree. According to ...

  4. Rem与Px的转换[转载]

    原文:http://www.w3cplus.com/preprocessor/sass-px-to-rem-with-mixin-and-function.html rem是CSS3中新增加的一个单位 ...

  5. Struts2+Spring+Hibernate实现员工管理增删改查功能(一)之登录功能

    昨天的博客中我分享了个人关于ssh实现员工管理的框架整合,今天我在分享管理员登录功能的实现.  转载请注明出处"http://www.cnblogs.com/smfx1314/p/78013 ...

  6. TOMCAT启动到一半停止如何解决

    当你的项目过大的时候,往往会导致你的TOMCAT启动时间过长,启动失败,遇到该情况可以试一下下面两招: TOmcat启动到一半的时候停止了,以下原因: 1.  tomcat启动时间超过了设置时间: 解 ...

  7. app.config 配置多项 配置集合 自定义配置

    C#程序的配置文件,使用的最多的是appSettings 下的<add key="Interval" value="30"/>,这种配置单项的很方便 ...

  8. middlewares in GCC

    Our GCC is a project developed by React that makes it painless to create interactive UIs. Design sim ...

  9. c#异步调用的几种方式

    首先,我们分析一下异步处理的环境 需要在当前线程中获取返回值 不需要在当前线程中获取返回值,但是仍然需要对返回值做处理对于第1中情况,还可以继续细分 在当前线程中启动线程T,然后继续执行当前线程中的其 ...

  10. ionic基本环境的搭建

    1.下载版本大于6的Node.js https://nodejs.org/en/ 个人喜欢下载最新版本 安装成功后可以用命令行工具输入node -v和npm -v分别查看node.npm版本 2.下载 ...