设计模式——装饰器模式

549 阅读5分钟

装饰器(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加方法或属性。与继承相比,装饰者是一种更轻便灵活的做法。

简单说:允许向一个现有的对象添加新的功能,同时又不改变其原有结构。

简单的装饰器模式

我们在修改老项目,或者与同事合作开发一个任务的时候,会发现在一份你无法更改的js文件里定义了window.onload方法,而你也需要在window.onload方法里做些操作,那么这种情况下改怎么办呢?

// 同事的代码
window.onload = () => {
    console.log('同事的处理逻辑')
}

// 装饰者
let _onload= window.onload || function () {} // 将原函数缓存下来
window.onload = () => {
    _onload()
    console.log('自己的处理函数')
};

但是上述写法也有几个弊端:

1)需要多维护了一个中间变量_onload,当装饰链比较长或者需要装饰的函数变多的时候,中间变量会越来越多。

2)会有this被劫持的风险。

针对上述两个弊端,接下来就介绍一个AOP函数来优化这种装饰:

AOP装饰函数

AOP装饰函数是把一些跟核心业务逻辑无关的功能抽离出来,比如说日志统计等;也可以执行一些业务逻辑的前置操作,比如说参数校验等。

例如我们平时写业务逻辑的时候,通常会在最后加上埋点逻辑,如下:

function event () {
    console.log('业务逻辑');
    console.log('埋点逻辑');
}
event();

这种写法在功能上没有什么问题,但是其实我们可以把业务逻辑和埋点逻辑分离开,让代码逻辑显得更清晰。

Function.prototype.after = function(afterfn) {
    let _this = this;   // 将原函数的引用缓存下来
    return function () {   // 返回一个添加了自己逻辑(afterfn)的新函数
        let ret = _this.apply(this, arguments)   // 执行原函数,并且保存住返回结果
        afterfn.apply(this, arguments)   // 执行自己的函数,并且保证this不会被劫持,同时自己的函数接受的参数和原函数一样
        return ret
    }
}
function event () {
    console.log('业务逻辑');
}
// 利用after装饰器给event加上埋点逻辑
event = event.after(() => {
    console.log('埋点逻辑');
})
event();

再次优化onload方法

利用上面的AOP装饰函数,我们回过头来看下最初对window.onload的装饰,针对前面提出的两点弊端,还可以再优化一下:

Function.prototype.after = function(afterfn) {
    let _this = this
    return function () {
        let ret = _this.apply(this, arguments)
        afterfn.apply(this, arguments)
        return ret
    }
}
window.onload = function(){
    console.log('同事的处理逻辑')
}
window.onload = ( window.onload || function(){} ).after(function(){
    console.log('自己的处理函数')
}.after(function(){
    console.log('自己的第二个处理函数')
});

而且这种写法是可以在后面不停加 .after(function () {}) ,可以连续的加上各个逻辑块。

说到装饰器模式,其实很容易联想到ES7的装饰器,那么我们来简单看看ES7的装饰器。

ES7的装饰器

我们自己先建一个class,作为基础类。

class Base {
    constructor(val = 1){
        this.init(val);
    }
    init (val) {
        this.val = val;
    }
    getVal() {
        console.log(this.val)
    }
}
var base = new Base();
console.log(base.getVal()); // 1

现在我们要做在Base初始化数据时默认加上100作为基数,可以给init方法加个装饰器,写法如下:

function decorateInit(target, key, descriptor) {
    const method = descriptor.value;  //method就是init的原函数
    let baseNum = 100;
    descriptor.value = (...args)=>{ // 重写原函数
        args[0] += baseNum;
        let ret = method.apply(target, args); // 将修改后的参数传递给原函数
        return ret;
    }
    return descriptor;
}
class Base {
    constructor(val = 1){
        this.init(val);
    }
    @decorateInit
    init (val) {
        this.val = val;
    }
    getVal() {
        console.log(this.val)
    }
}
var base = new Base();
console.log(base.getVal()); // 101

首先看到在init方法上多了个 @decorateInit ,这个其实就是装饰器。

而在 decorateInit 方法中接受3个参数,这里插一个小知识点。在ES6的class语法糖中“”

class A {
    sayHello () {
        console.log('hello world');
    }
}

在给类添加方法时,其实是调用 Object.defineProperty 这个方法,而 Object.defineProperty 方法我们都知道接受三个参数,target 、name 和 descriptor。所以创建一个Class并且添加一个方法等价于下面这段代码。

function A () {}
Object.defineProperty(A.prototype, 'sayHello', {
    value: function() { console.log('hello world'); },
    enumerable: false,
    configurable: true,
    writable: true
})

而装饰器也是利用了 Object.defineProperty,所以传参也就一致了。第一个参数为类的原型对象,第二个参数为要装饰的属性名,第三个参数是该属性的描述对象。当然,装饰器也是可以传参的,看下面一种写法:

function decorateInit(num)
    return function (target, key, descriptor) {
        const method = descriptor.value;  //method就是init的原函数
        let baseNum = num || 100;
        descriptor.value = (...args)=>{ // 重写原函数
            args[0] += baseNum;
            let ret = method.apply(target, args); // 将修改后的参数传递给原函数
            return ret;
        }
        return descriptor;
    }
}
class Base {
    constructor(val = 1){
        this.init(val);
    }
    @decorateInit(20)
    init (val) {
        this.val = val;
    }
    getVal() {
        console.log(this.val)
    }
}
var base = new Base();
console.log(base.getVal()); // 21

其实就是在原有的装饰器函数外再嵌套一层函数。

Vue中的装饰器

利用前面的知识,我们先写个AOP装饰函数(after)绑定在Function原型上,然后在装饰器(log)中重写函数(装饰器模式),以达到先执行业务逻辑,再执行装饰器中的埋点逻辑的目的。最后在Vue的methods里给具体的方法加上装饰器。具体代码如下:

// 第一部分就是AOP装饰函数
Function.prototype.after = function (afterfn) {
    let _self = this;
    return function () {
        let ret = _self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    };
};
// 第二部分是一个能接收参数的log装饰器函数
const log = function (stat) {
    return function (target, key, descriptor) {
        descriptor.value = descriptor.value.after(() => {
            console.log('埋点逻辑' + stat)
        });
    };
};
// 以上两个方法其实可以封装在基础类中

// Vue项目组件中可以添加装饰器
export default {
    name: 'List',
    data () {
        return {}
    },
    methods: {
        @log('123456')
        event () {
            console.log('业务逻辑');
        }
    },
};

装饰器模式和代理模式的区别

装饰器模式和代理模式都是对现有对象功能的扩展,但是两者的出发点不同。装饰者模式偏向于增加对象功能的同时做到解耦;代理模式偏向于对对象的辅助或者流程的把控。

总结

装饰器模式的优点:

1、可以动态扩展一个对象;

2、复用性较强。可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器来装饰不同的对象。

缺点:

1、多层装饰使用起来相对比较复杂。