AOP(Aspect-Oriented Programming)

写在前面

时不时地总会在各种地方看到AOP,wiki也查了不止一次,但每次都对那一堆陌生术语望而却步,这次总算下决心要尝试AOP了

最后发现,AOP类似于设计模式,不同于策略模式模板方法模式装饰者模式。AOP的近亲是代理模式,同样能够分离逻辑,核心也是拦截与细节隐藏

P.S.搬出来这么多名词其实不是故意的,因为在理解AOP的过程中确实有对比思考过这几个模式。然后,发现设计模式这种东西,嗯,怎么说呢,有用吗?没有用吗?额

一.术语(Glossary)

  • 切面(Aspect)

    在AOP中表示为“在哪里做和做什么的集合”

    横切关注点的模块化,比如上边提到的日志组件。可以认为是增强、引入和切入点的组合。例如日志、缓存、传输管理

  • Join point(连接点)

    在AOP中表示为“在哪里做”

    表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等。表示执行期的一个点,例如方法执行或者属性访问

  • 增强(Advice)

    在AOP中表示为“做什么”

    或称为增强在连接点上执行的行为,增强提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段。包括前置增强(before advice)、后置增强 (after advice)、环绕增强 (around advice)。表示切面在特定连接点处的动作

  • 切入点(Pointcut)

    在AOP中表示为“在哪里做的集合”

    选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法。用来匹配连接点的正则表达式,增强都有相关的切入点表达式,在任何与之匹配的连接点处执行,例如,某个特定名称的方法的执行

  • 引入(Introduction)

    在AOP中表示为“做什么(新增什么)”

    也称为内部类型声明(inter-type declaration),为已有的类添加额外新的字段或方法

  • Weaving(织入)

    把切面和其它应用程序类型或者对象链接起来,以创建增强对象

    织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行

  • 目标对象(Target Object)

    在AOP中表示为“对谁做”

    需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被增强的对象,从而也可称为“被增强对象”

  • AOP代理(AOP Proxy)

    AOP框架使用代理模式创建的对象,从而实现在连接点处插入增强(即应用切面),就是通过代理来对目标对象应用切面

术语比较多,简单分类:

抽象概念:切面、引入、织入、目标对象、AOP代理

具体概念:连接点、增强、切入点

关系:切入点是连接点形成的集合,两者都表示需要插入逻辑的目标位置,增强表示需要插入的具体动作

使用AOP时需要关注的是连接点和切入点,前者是“想在哪个位置插入逻辑”,后者是“想在哪块区域插入逻辑(区域由位置组成)”,再切入并注册advice,添加前置后置逻辑

Advice类型

  • 前置增强(Before advice)

    在某连接点之前执行的增强,但这个增强不能阻止连接点前的执行(除非它抛出一个异常)

  • 后置返回增强(After returning advice)

    在某连接点正常完成后执行的增强:例如,一个方法没有抛出任何异常,正常返回

  • 后置异常增强(After throwing advice)

    在方法抛出异常退出时执行的增强

  • 后置最终增强(After (finally) advice)

    当某连接点退出的时候执行的增强(不论是正常返回还是异常退出)

  • 环绕增强(Around Advice)

    围绕一个连接点的增强,如方法调用。这是最强大的一种增强类型。环绕增强可以在方法调用前后完成自定义的行为。它也负责选择是继续执行连接点,还是直接返回它们自己的返回值或者抛出异常来结束执行

需要注意的是AfterThrowing与AroundAdvice的区别,业务逻辑发生异常后,会触发前者,但拿不到异常对象,只知道关注的方法发生异常了,意义不大。而后者是把业务逻辑完全包裹起来,所以可以捕获异常信息(暂不讨论异步回调异常)。其它几种Advice都是字面意思,很容易理解

二.作用

AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性

感受一个例子,面向对象代码很容易长成这样:

/**
 * OO style
 */
class OOUser {

    public function add($fields) {
        // check auth
        if (!$this->isGranted('ADD_USER')) {
            throw new Exception("Access Denied");
        }

        // log
        $this->log('creating user');

        // create user
        try {
            $user = array('id' => '003');
            $user['name'] = 'user';
            //...

            // save
            $this->insertUser($user);
        } catch(Exception $e) {
            $this->log('user create error: ' + $e);
            // handleError($e);
        }

        // log
        $this->log('user created');
    }
}

缓存,日志,异常处理,权限检查等逻辑分散穿插于项目代码各处(不止User类),无法分离出来。存在很多问题:

  • 无法复用

  • 难以理解类的最初职能,逻辑杂乱

  • 很容易出错,如果忘记写这些样板(biolerplate)代码的话

  • 有违DRY原则,每个逻辑块都穿插着这些面熟的代码

尤其是维护老项目,看到一块块的脸熟代码很难受,想改又抽不出来,或者费了很大劲最后只是缓解了一点表面症状(比如,考虑其它封装方式,精简了几行业务代码)

AOP专门解决这个问题,它可以横向切入对象内部进行内科手术,剥离核心业务逻辑,我们就可以专注于真正有用的那几行代码

三.PHP AOP示例

发现了一个比较好用的PHP AOP框架:Go! AOP

P.S.因为较好的AOP框架涉及反射与注解,以笔者目前的PHP能力不足完成,所以放弃了手动实现AOP机制的想法

考虑之前的OO代码,对逻辑块进行分类:

public function add() {

//=before advice
    // check auth
    if (!isGranted('ADD_USER')) {
        throw new Exception("Access Denied");
    }

//=before advice
    // log
    log('creating user');

//=business logic
    // create user
    try {
        $user = array('id' => '003');
        $user['name'] = 'user';
        //...

        // save
        insertUser($user);
    } catch(Exception $e) {
//=after throwing advice
        log('user create error: ' + $e);
        // handleError($e);
    }

//=after advice
    // log
    log('user created');
}

发现业务逻辑只有几行,但是,被其它不很关键的代码深深地包起来了,更新维护时就将面对“在一大片代码中修改某一小块”的问题,定位到关键部分再小心翼翼地修改,然后还是很容易出错(尤其是异常处理)

然后抽离业务逻辑,新的User类是这样的:

/**
 * AOP style
 */
class AOPUser {

    public function add($fields) {
//=business logic
        // create user
        $user = array('id' => '003');
        $user['name'] = 'user';
        //...

        // save
        $this->insertUser($user);

        // throw error
        $this->badMethod();
        throw new Exception("A Stange Error");
    }

    /**
     * Insert user to database
     *
     * @Loggable
     * 
     * @param  Array $info Info
     */
    public function insertUser($user) {
        //...
        $this->log('user inserted');
    }
    //...其它无法共享的依赖方法
}

我们把业务逻辑分离出来了,可共享的依赖方法(比如,log(), isGranted()等)都被抽出来成为共享lib,其它无法共享的依赖方法仍然作为类成员存在,此时User类的职责相对单一,不和谐的代码都滚出去了,逻辑很清晰

接下来需要装配(类似于装饰者模式,但实现方式上差异较大),把滚出去的相关代码再装上,AOP会帮我们动态组装,我们只需要声明关联,告诉AOP在哪里 装什么(也就是术语“切面”的含义)

/**
 * User aspect
 */
class UserAspect implements Aspect {

    /**
     * Pointcut for add method
     *
     * @Pointcut("execution(public App\App\AOPUser->add(*))")
     */
    protected function UserAdd() {}
    // 执行$aopuser->add()时切入

    /**
     * Check anth before add user
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("$this->UserAdd")
     */
    protected function checkAuthBeforeAdd(MethodInvocation $invocation) {
        /** @var $user \App\App\AOPUser */
        $user = $invocation->getThis();
        // check auth
        if (!$isGranted('ADD_USER', $user->caller)) {
            throw new Exception("Access Denied");
        }
    }

    /**
     * Handle Error after throwing
     *
     * @param MethodInvocation $invocation Invocation
     * @AfterThrowing("$this->UserAdd")
     */
    protected function handleErrorAfterThrowing(MethodInvocation $invocation) {
        /** @var $user \App\App\AOPUser */
        $user = $invocation->getThis();
        // =after throwing advice
        $log('user create error, handle error here');
        // handleError();

        //!!! avoid reporting error
        set_exception_handler(function($e) {
            echo "!!!Global Exception Handler: " . $e->getMessage();
        });
    }

    /**
     * Log after add user
     *
     * @param MethodInvocation $invocation Invocation
     * @After("$this->UserAdd")
     */
    protected function logAfterAdd(MethodInvocation $invocation) {
        /** @var $user \App\App\AOPUser */
        $user = $invocation->getThis();
        // log
        $log('user created');
    }

    //...其它Advice
}

通过注解声明增强(Advice)与目标对象的联系,告诉AOP在哪里插入什么逻辑,消除逻辑粘连

注意handleErrorAfterThrowing()方法,为了避免全局异常报错,我们使用了set_exception_handler(),这样做是因为AfterThrowing增强在切点发生异常时会触发,但我们拿不到异常对象,也无法吃掉它,所以通过全局异常拦截来吃掉这个异常

如果需要精确操作某过程中的异常,应该使用Around增强,把目标过程完全包裹起来,再try-catch即可,如下:

/**
 * Around advice to catch exception
 * @param  MethodInvocation $invocation Invocation
 * @Around("execution(public App\App\AOPUser->badMethod(*))")
 */
protected function aroundBadMethod(MethodInvocation $invocation) {
    try {
        $invocation->proceed();
    } catch (Exception $e) {
        echo '!!!Around Advice Error Handler: ' . $e->getMessage() . "<br>\n";
    }
}

P.S.此处不讨论异步回调中的异常,PHP一般不关注这种情况,JS的话,也不考虑注解方式实现AOP(应该采用高阶函数、binding之类的方式进行逻辑注入),以后再说

P.S.GO! AOP相当强大,也提供了切入系统方法及工具函数的方式,包括参数截获,属性访问拦截等等

四.Demo

在线Demo:http://www.ayqy.net/temp/aop/src/

源码地址:https://github.com/ayqy/aop

注意:需要PHP5.5+,因为GO! AOP框架内使用了class作为标识符

五.总结

AOP是对OOP的补充,横向切入对象并进行逻辑注入,确保类的职责单一

更贴切地说,AOP是一种设计模式,也有比较激烈的看法:

AOP是OOP的补丁,纵向OOP建立对象体系,继承封装多态;横向AOP切入,纵横合璧,天下无敌…

也没错,只是存在侵入程度的争议,比如,如果想要AOP切入整个OO体系,势必侵入程度很大(考虑继承)。个人更喜欢侵入程度小的方案,灵活但不方便

怎么说,学习AOP算是获得了一种设计思路,类似于设计原则(复习一下):

  • 封装变化(把易于发生变化的部分抽出来,以减少其变化对其它部分的影响)

  • 多用组合,少用继承(组合比继承更有弹性)

  • 针对接口编程,不针对实现编程(使用接口可以避免直接依赖具体类)

  • 为交互对象之间的松耦合设计而努力(更松的耦合意味着更多的弹性)

  • 类应该对扩展开放,对修改关闭(open-close原则)

  • 依赖抽象,不要依赖具体类(减少对具体类的直接依赖)

  • 只和朋友交谈(密友原则)

  • 别找我,我会找你(Don’t call me, I will call you back.安卓开发的大原则)

  • 类应该只有一个改变的理由(单一责任原则)

  • 横向逻辑注入(AOP)

考虑问题时多一种选择,仅此而已。在构建大型系统时AOP应该是必要的内置功能,但就应用场景而言,AOP并不是万能钥匙,但AOP的思想(横向逻辑注入)适用于任何场景

参考资料

AOP(Aspect-Oriented Programming)》上有2条评论

  1. 昊少年

    看到最后发现把设计模式的设计原则复习了一遍,另外这个东西术语根本看不懂啊,不看demo谁知道他要干什么。

    回复
    1. ayqy 文章作者

      最后只是顺便复习设计原则,表明AOP的思想像设计原则一样,可以广泛应用,而不是说本文落脚点是推销设计原则,本文只是想说明AOP是什么

      对于设计原则,确实需要结合实例场景才能理解,可以查看设计模式学习笔记

      至于设计模式,开头就说了“有用吗?没有用吗?”,目前个人感觉用处不大,但可能与日常工作偏业务,还没有触及设计层次有关,体会不到这些模式的作用(解决业务问题不太可能需要设计模式支持,甚至设计原则应用也相对较少)。但肯定有用,至少能用来分析别人的代码,比如探究源码时还是能感受到的

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code