装饰器模式:分离业务代码和数据统计代码

347 阅读5分钟

装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。

装饰函数

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

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

const func = function() {
	console.log('我是Func的原有逻辑');
}

// 改成

const func = function() {
	console.log('我是Func的原有逻辑');
  console.log('我是Func的装饰器逻辑');
}

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

用AOP装饰函数

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

Function.prototype.before = function( beforefn ){
  let __self = this;  // 保存原函数的引用
  return function(){    // 返回包含了原函数和新函数的"代理"函数
    beforefn.apply( this, arguments );  // 执行新函数,且保证this不被劫持,新函数接受的参数
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
    return __self.apply( this, arguments );  // 执行原函数并返回原函数的执行结果,
    // 并且保证this不被劫持
  }
}

Function.prototype.after = function( afterfn ){
  let __self = this;
  return function(){
    let ret = __self.apply( this, arguments );
    afterfn.apply( this, arguments );
    return ret;
  }
};

Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。

接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。

我们注意到,通过Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持。

用AOP动态改变函数的参数

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

const request = function(type, url, params) {
	// 发送请求代码...
}

request('get', 'http://xxx.com/userinfo', {name: 'gosnails'});

request函数在项目中一直运转良好。直到有一天,我们的网站遭受了CSRF攻击。解决CSRF攻击最简单的一个办法就是在HTTP请求中带上一个Token参数。

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

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

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

const request = function( type, url, param ){
  param = param || {};
  Param.Token = getToken();     
  // 发送请求代码...
};

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

为了解决这个问题,把Token参数通过Function.prototyte.before装饰到request函数的参数param对象中:

request = request.before(function( type, url, param ){
  param.Token = getToken();
});

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

装饰类

在项目开发的结尾阶段难免要加上很多统计数据的代码,假如页面中有一个Button,点击这个Button除了业务代码,与此同时还要进行数据上报,来统计有多少用户点击了这个Button。

传统面向对象装饰器

class Button {
	onClick() {
  	console.log('我是业务逻辑');
  }
}

class classDecorator {
  constructor(button) {
  	this.button = button;
  }
	onClick() {
    this.button.onClick();
  	console.log('我是数据上报逻辑');
  }
}

// 验证装饰器是否生效
let button = new Button();
button = new classDecorator(button);

button.onClick();

装饰类的构造函数接受参数button对象,并且保存好这个参数,在它的onClick方法中,除了执行自身的数据上报逻辑之外,还调用button对象的onClick方法。

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。

ES7 中的装饰器

在 ES7 中,我们可以通过一个@语法糖轻松地给一个类装上装饰器:

// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
    target.hasDecorator = true;
  	return target;
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
  // Button类的相关逻辑
}

// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator);

也可以用同样的语法糖去装饰类里面的方法:

function funcDecorator(target, name, descriptor) {
  let originalMethod = descriptor.value;
  descriptor.value = function() {
    let ret = originalMethod.apply(this, arguments);
    console.log('我是数据统计逻辑');
    return ret;
	}
  return descriptor;
}

class Button {
  @funcDecorator
  onClick() {
  	console.log('我是业务逻辑');
  }
}

// 验证装饰器是否生效
const button = new Button();
button.onClick();

总结

通过动态改变函数参数、数据上报这两个例子,我们了解了装饰函数和装饰类,这种模式在实际开发中非常有用,装饰器模式可以避免为了让框架拥有更多的功能,而去使用一些if、else语句预测用户的实际需要。