JavaScript JS 小试 AOP 编程

lonhon · 2018年02月26日 · 833 次阅读
本帖已被设为精华帖!

AOP为Aspect Oriented Programming的缩写,译为:面向切片编程。

本文主要从个人理解讲述AOP原理、与面向对象OOP的比较、JavaScript中AOP的实现。

什么是AOP面向切片(亦称切面)编程

我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。 但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许它们(日志代码部分)是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

所以,** 实现动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切片的编程。 **

AOP与OOP比较

AOP编程思想也是面向对象编程的一种扩维补充,从扁平到立体。 一直以来,OOP要求我们将万物对象化,对象化后抽取其特点(属性、方法),达到重用性、灵活性和扩展性的目的。OOP从横向上区分出类,可以看做将对象扁平化。就好像抽象一个人,我们定义其方法(呼吸、跑、吃)、属性(身高、年龄、体重),就是把他们划成一块块单独的部分,封装之后就可以看做一个“人”。

而AOP编程思想之所以称作对OOP的扩维补充,是因为我们不改变对象原来的OOP架构达到为其添加原本不属于它的功能,且能够很方便的解耦。譬如小贾是一个“人”的实例,这时我们想让他有一个“飞翔”的功能,让一个“人”实例拥有不属于自身的“飞翔”功能,我们可以通过“多态”去实现,但是 这种方式其实已经在一定程度上破坏了原实例。 作为开发者,我们想让每个实例保持原有的特性,让小贾还是小贾。这时AOP编程思想就发挥作用了,从技术上来说AOP是通常使用代理机制实现为OOP扩维,让OOP保持其扁平,AOP使其立体。

使用过PhotoShop的朋友都知道PS中的图层,OOP与AOP可以看做PS中的图层和蒙版的关系,蒙版不破坏图层在其上面添加样式。 AOP之所以称为切片或切面,就相当于蒙版为图层添加更多的视觉效果而不改变图层本身

JavaScript中的AOP实现

先上一段原始代码,可将其放大为我们开发中的任意功能模块:

var dosome = function(v){ //功能代码
    console.log(`----------${v}----------`);
}

此时,我们想这个函数在每次使用之前添加使用日志,最常见的情况就是将日志模块的代码嵌入到dosome方法中 这种方式的弊端有以下:

  • 如果同时对多个方法添加日志,此时就需要在每个方法中添加代码
  • 对原代码具有破坏性,为dosome方法硬加上了不属于它的日志代码
  • 不够灵活,修改时可能需要对每个方法进行检查

可能会有朋友会说,我们将日志模块封装好后只需要一句代码就可以很方便达到目的,为什么还要用AOP的方式。 请记住 *** 不破坏原对象本身 *** 面向切片完成的是 不改变原对象而达到扩展功能 的目的。

现在,我们在dosome基础上,用AOP的方式实现在每次调用dosome方法前打印一段话

var addAOPbefore = function(fn,bfn){    //前置切面
    function newFn (){
        bfn.apply(this,arguments);//保证同一上下文
        fn.apply(this,arguments);//原方法正常执行
    }
    return newFn; //狸猫换太子
}
function dosomeBefore(){
    console.log(`本次调用方法有 ${arguments.length} 个参数`);
}

dosome = addAOPbefore(dosome, dosomeBefore);
dosome(6,7,8,9)

//执行结果
本次调用方法有 4 个参数
----------6----------

此时,dosome方法成功添加前置切面,接下来我们实现后置切片就很简单了,*** 注意:此方式有缺点 ***

var addAOPafter = function(fn,afn){    //后置切片
    function newFn (){
        fn.apply(this,arguments);//原方法正常执行
        afn.apply(this,arguments);//其实就是调换了位置
    }
    return newFn; //狸猫换太子
}
function dosomeAfter(){
    console.log('本次调用方法完成');
}

dosome = addAOPbefore(dosome, dosomeBefore);
dosome(6)

//执行结果
----------6----------
本次调用方法完成

现在来将addAOP的方法进行封装,实现一个前后切片的工厂。

//双切 
var addAOP = function(opt){
    function newFn(){
        opt.bfn && opt.bfn.apply(this,arguments);
        opt.fn.apply(this,arguments);
        opt.afn && opt.afn.apply(this,arguments);            
    }
    return newFn;        
}

dosome = addAOP({
            fn: dosome,
            bfn: dosomeBefore,
            afn: dosomeAfter
        });

dosome(666)    
//执行结果
本次调用方法有 1 个参数
----------6----------
本次调用方法完成

ok 一个工厂型的切片添加方法已经制成,现在来说完成前置切片实现时所说的“缺点”

** 原方法的返回值跑哪里去了??? ** 比如我现在的dosome返回是一个Promise,我们还需要在它的基础上做then、catch等操作,用上面代码来完成AOP你会发现 then is not a function of "undefined"

现在我们来聚焦双切方法的内部代码

function newFn(){//新方法
    opt.bfn && opt.bfn.apply(this,arguments);
    opt.fn.apply(this,arguments);
    opt.afn && opt.afn.apply(this,arguments);            
}
return newFn;//狸猫换太子

这里我们可以看到,我们只是将狸猫换成了太子,并没有将太子身上的东西给狸猫带上 所以,我们需要将源方法的返回值保留,并由newFn返回,改良后的代码如下:

function newFn(){//新方法
    opt.bfn && opt.bfn.apply(this,arguments);
    let res = opt.fn.apply(this,arguments);
    opt.afn && opt.afn.apply(this,arguments);            
    return res; //返回太子所携王八之气
}
return newFn;//有了王八之气,狸猫==太子

经过这步,我们就可以把狸猫当做太子使用了。

结语

其实OOP、AOP等编程思想,都是我们对实战中重复、冗余的代码进行结构优良化, AOP算是对OOP编程的一种有益补充

共收到 0 条回复
es6china 将本帖设为了精华贴 02月27日 10:41
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册