浅谈设计模式之装饰器模式(一)

206 阅读5分钟

移花接木

历史上有个国家叫做黄国,被楚国灭亡之后部分遗民隐忍生息,活了下来,并且通过各种方式成为了楚国的大臣,伺机报亡国之仇。当时的楚国皇帝楚烈王荒淫过渡却并没有生育能力,于是申君黄歇公(旧黄国遗民)就联合门客李园,在和李园的妹妹嫣嫣相处过一段时间,等到确定嫣嫣怀孕之后,将嫣嫣送给了楚王,这样,楚国的大权实际上落在歇公后代手中。那各位说了,好好的谈装饰器模式这怎么说上故事了。各位看官不要急,这前端装饰器模式的实现核心其实就在这几个字上,移花接木。

装饰器模式

闲话少说,进入正题。装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构。重点体现了设计模式六大原则之中的单一职责原则和开闭原则。单一职责很好理解,就是专心,就是一个函数只做一件事情。开闭原则是指要对扩展开放,对修改关闭。

说的简单些就是包装原有的类,在不改动原有功能的前提下,扩展新的需求和功能。

快速体验

假设现在我们有一个现有函数如下

let run = function(){    
    console.log("version-1.0")
}

现在我们需求是对其扩展一个新的功能,最快的方法当然就是在 run 函数里添加一段新功能代码。但是这就违反了设计模式的单一职责原则和开闭原则。这就需要我们装饰器模式大展身手了。

let run = function(){
    console.log("version-1.0")
}
const _run = function(fn){
    return function(){
        console.log("新加的功能version-2.0")
        fn()    
    }
}          
run = _run(run)run()

对于原函数,我们没有做任何的修改,因此不必担心原有功能受到影响。首先将原函数对象作为参数传入我们定义的新功能函数,新功能函数返回的一个新函数,这个函数加入我们需要增加的功能,然后执行上面传入的原函数,然后重写原函数变量的地址,赋值为这个新函数。最后执行上面更改过的原函数。这样就实现了一个最简单的装饰器模式。是不是看到这,有了移花接木的味道。

为了加深我们对装饰器的印象和认知,我们下面将它和一些另外的模式作比较。

装饰器模式与代理模式

咋的一看,大家会不会觉得这装饰器模式和代理模式好像啊,都是不改变原有功能的情况下增加新的功能,但细细一想他们还是有区别的。代理模式是控制控制访问,而装饰器模式是新增行为。装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

ES6 中就提供了一个新的标准内置对象 Proxy,就是代理模式的实现。在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。具体实现如后续有需求会自行补上,此处就不再做过多详细介绍。

装饰器模式与面向切面编程

可能有些前端的朋友们可能会好奇,面向切面编程是啥玩意啊,那我换个说法,koa框架的洋葱模型(恍然大悟状:原来是这玩意啊)。

没错,面向切面编程也是一种编程思想,而洋葱模型中间件的处理流程就是这种思想的实现。假设我有一个函数方法就是洋葱的核心,我想在方法调用前后做一些事情,比如打日志、算时间等等,这时候就将我们的函数方法包裹起来,再加一层,这就是面向切面编程。思考的是程序执行的位置。面向对象编程可以让我们方便抽象和管理我们的代码,而面向切面编程的重点在于解耦和复用。

装饰器也是实现面向切面编程的一种方式。

洋葱模型的简单实现

const App = function () {  
    // 中间件公共的处理数据  
    let context = {}  // 中间件队列  
    let middlewares = []  
    return {    
        // 将中间件放入队列中    
        use (fn) {      
            middlewares.push(fn)    
        },    
        // 调用中间件    
        callback () {
            //koa2源码中放在 http.createServer(callback) 回调中调用      
            // 初始调用 middlewares 队列中的第 1 个中间件      
            return dispatch(0)      
            function dispatch (i) {        
                // 获取要执行的中间件函数        
                    let fn = middlewares[i]        
                    // 执行中间件函数,回调参数是:公共数据、调用下一个中间件函数        
                    // 返回一个 Promise 实例        
                    return Promise.resolve(          
                        fn(context, function next () { dispatch(i + 1) })        
                    )      
            }    
        },  
    }
}

function.before 和 function.after

比较经典的 AOP 实现方式就是 function.before 和 function.after,同样我们也可以用他们来实现装饰器模式。

Function.prototype.before = function(fn){
    var _this = this;       // 用来保存调用这个函数的引用,如func_1调用此函数,则_this指向func_1    return function(){      // 返回一个函数,这个函数包含原函数和新函数,原函数指的是func_1,新函数指的是fn
        fn.apply(this,arguments);   // 执行新函数
        return _this.apply(this,arguments);     // 执行原函数
    }
}
Function.prototype.after = function(fn){
    var _this = this;
    return function(){
        var r = _this.apply(this,arguments); // 先执行原函数,也就是func_1
        fn.apply(this,arguments);   // 再执行新函数
        return r;
    }
}
var func_1 = function () {
    console.log("2")
}
func_1 = func_1.before(function () {
    console.log("1");
}).after(function () {
    console.log("3");
} )
func_1();   // 输出1、2、3

es7的装饰器

本质上还是用object.defineProperty(),浏览器是无法识别的,需要babel编译,装饰器的具体实现,感兴趣的朋友们可以使用bable看编译之后的原理

function nameDecorator(target, key, descriptor) {
    descriptor.value = () => {
        return 'jake';
    }
    return descriptor;
}
class Person {
    constructor() {
        this.name = 'jake'
    }
    @nameDecorator
    getName() {
        return this.name;
    }
}

let p1 = new Person();
console.log(p1.getName())

文章持续更新中...............