简析面向切面编程(AOP)

看到 AOP,那么一定会联想到 OOP,也就是面向对象编程。AOP 和 OOP 是在不同业务场景下的两种设计思想,本文将尽量用生动有趣的语言介绍 AOP 在前端领域的应用以及 AOP 和 OOP 之间的区别。在这之前,我们先了解两者的基本概念。

1.面向对象编程(OOP)

对于面向对象编程的严格概念,每个人都有自己的理解,笔者也不在此二次解读了。但是我们可以举一个生活中的例子来类比,为后文做铺垫:

有这样一个女孩,她每个月底都要充话费。每次充话费的时候,她都要打开支付宝,填写手机号码,再选择充值金额,最后点击充值按钮完成充值。整个过程,我们可以称之为面向过程充值。

后来,她找了男朋友,给手机充话费的重担就落在了男朋友身上。于是女孩每次充值前,都会把充值金额发给男朋友,男朋友完成操作后告诉她充值好了。整个过程,我们可以称之为面向对象充值。因为女孩只需要关心充值金额和充值结果即可,不用关心具体怎么充值的。

2.面向切面编程(AOP)

面向对象编程好像已经能满足大部分场景了,可以扩展可以复用,那面向切面编程又是为了解决什么问题呢?基于上文的充值例子,我们拓展一下场景:

过了一段时间,女孩发现男朋友只会用支付宝充值话费,某天她想让男友用微信充值,但是发现市面上的手机无法同时安装微信和支付宝。女孩不得不又找了一个手机里有微信的男朋友。可是同时交往两个男朋友心力交瘁,女孩该怎么办呢?

面向切面充值提供了解决方案:女孩额外买了两支手机,一支装有微信,另一只装有支付宝,然后她甩掉了其中一个男朋友。这样每次她想让男朋友帮忙充话费的时候,就把两支手机中的一支给男朋友,并且使用微信还是支付宝,女孩可以自己决定。

讲到这里,相信读者脑海中对 AOP 和 OOP 的区别已经有了大致的轮廓了。AOP 的具体定义是什么?它是一种通过预编译或运行期动态代理实现逻辑功能统一维护的技术。OOP 是针对对象属性和行为的封装,而 AOP 是针对某个步骤和阶段的,上面例子的"男朋友"就是对象的封装,而"充值方式(支付宝/微信)"则是"充值"行为的逻辑片段。可以说,AOP 是对 OOP 的进一步增强。

AOP 有如下三个主要概念:

1.切面(Aspect)

这也是 AOP 首字母A的由来,切面 = 切入点 + 通知。

2.切入点(Pointcut)

切入点是指具体被增强的类或者方法。

3.通知(Advice)

在切入点执行时的增强处理,类似回调函数的作用,例如下文将要提到的 Dojo 的after等方法就是通知。

接下来我们写一段代码,来切实感受下以上三个抽象的概念。
假设我们有一个对象,其中含有一个打印的方法:

let TestObj = {
    log: () => {
        console.log('打印log');
    }
}

我们希望程序增加一个功能,在打印之前告诉我们要打印了,在打印之后也告诉我们执行结束,但是不能去改动对象内部的代码,那么我们可以编写一个新的方法:

let emitter = (target) {
    console.log('即将开始打印');
    target.log();
    console.log('打印结束');
}

emitter(TestObj);

上述代码是实现 AOP 最简单和典型的方式。emitter函数并没有改变原有对象的逻辑,仅仅做了“包装”和“增强”,那么一般把log方法称之为切入点,它前后执行的console.log称之为通知,emitter称为切片,而TestObj称为切面。

3.前端为什么需要AOP

AOP 在 Spring 框架中有的大量的应用,不过这并不属于前端范畴,我们姑且不做讨论。前端的 Dojo 框架可以算是早期在底层应用 AOP 思想的代表,它内部提供了 aspect 模块。 aspect 模块提供了三个方法:

  • after
  • before
  • around

从三个方法的名字中我们就能大概猜到他们的用处,以传输 ajax 请求为例,before用于发送请求前对参数做最后的处理,after用于处理 ajax 返回的数据,around则是在请求过程中使用。三个方法都不会改变核心代码的逻辑,仅仅起到“包装”的作用,和 JS 的前调函数和回调函数非常类似。来到现代前端开发,我们经常遇到和使用表单。当然,我们会把各式各样的业务表单封装成业务表单组件,因为这样能够很好地降低开发工作量。

可是有一天,产品经理要求每个业务表单组件提交时都需要埋点。于是我们决定将埋点逻辑添加到每个业务组件中,但是这些埋点逻辑是完全一样的,免不了复制粘贴。有没有什么办法复用埋点逻辑呢?依据 OOP 的思想,我们考虑将埋点逻辑抽象成一个单独的Log 类,并在业务组件内调用。但是这样又造成了副作用,Log 会与业务组件发生耦合,每当 Log 类发生改动时,所有的业务组件埋点行为都会发生变化,这又是令人头疼的。所以我们需要一种新的编码方式,可以动态地将 Log 埋点逻辑注入到指定组件内部甚至内部指定方法,解决方案与上面的示例代码思路相同,这种编程思想就是 AOP。

4.前端领域的AOP

1.javascript

因为 AOP并不是前端源起的概念,所以在 ES6 发布之前,只能使用原生 js 进行抽象模拟,而且一种常用的解决方法是改造Function的原型,极易出现 bug。而在 ES6 发布之后,装饰器(Decorator)作为正式标准引入了 Javascript。该标准是从其他面向对象语言中借鉴来的,对于前端开发而言,可以更加规范地通过 AOP 思想编写代码。与 python 类似,js 使用@关键字。我们来看下用法:

@decorator
class TestClass {
    // ...
}

function decotator(targetClass) {
  targetClass.rejectDecorator = true;
}

TestClass.rejectDecorator  // true

从上述代码可以看出,装饰器decorator修改了组件内部的变量,而装饰器实质上就是一个函数。既然是函数,那么必然可以通过传入的参数来控制装饰器的行为:

function proDecorator(boolValue) {
    return function(targetClass) {
        target.rejectDecorator = boolValue;
    }
}

@proDecorator(true)
class TestClass() {}
TestClass.rejectDecorator   // true

@proDecorator(false)
class TestClass() {}
TestClass.rejectDecorator   // false


装饰器不但可以修饰类,也可以修饰类中的方法。我们上面提到的 Log 问题,就可以通过使用装饰器修饰内部方法来解决:

class TestClass {
    @log
    add(a, b) {
        return a + b;
    }
}

function log(target, name, descriptor){
    let oldValue = descriptor.value;

    descriptor.value = function() {
        console.log(`Calling ${name} with`, arguments);
        return oldValue.apply(this, arguments);
    };

    return descriptor;
}

const Test = new TestClass();

const num = Test.add(2, 4);
console.log(num);

// calling add with [object Arguments] { ... }
// 6

装饰器用法还有很多,感兴趣的读者可以自行查阅,本文不再赘述。通过上述代码我们发现,装饰器很好地实现了 AOP 所倡导的设计理念,同时其本身又可以参数定制化,摆脱了传统函数式工具的限制。

2.React

对于 React 熟悉的读者应该听过 Mixin,它是一种组织 React 内部函数的方法。而后来 Facebook 官方逐渐弃用了这种模式,推荐开发者使用高阶组件(HOC)。其实 HOC 就是一种典型的在 AOP 思想指导下的编码方式,我们看如下代码:

//WrappedComponent为被处理组件
function HOC(WrappedComponent){
    return class HOC extends Component {
        render(){
            const newProps = {type:'HOC'};
            return <div>
                <WrappedComponent {...this.props} {...newProps}/>
            </div>
        }
    }
}

@HOC
class OriginComponent extends Component {
    render(){
        return <div>这是原始组件</div>
    }
}

//const newComponent = HOC(OriginComponent)

WX20180817-115140

我们看到,在HOC函数内部声明了新的newProps,在render时将其添加到原始组件的props中。本身 React 组件就是类的一种,那么@HOC作用与装饰器作用完全相同。在 AOP 思想指导下,我们想对原始组件进行改造,比如增加一些props,不必亲自去改动内部代码,在组件外部添加装饰器即可。

5.总结

AOP 和 OOP 本身并不矛盾,在软件开发中,我们可以基于 OOP 对代码结构进行纵向划分模块,同时基于 AOP 横向注入逻辑单元,从而使软件结构更为立体。两者相辅相成,才能提高软件工程的健壮性。

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。