聊聊程序设计思想之面向切面编程AOP

2018-09-12 16:39:38 浏览数 (1)

一、官方解释什么是AOP编程

代码语言:javascript复制
AOP:Aspect Oriented Programming,译为面向切面编程。在不修改源代码的情况下,通过运行时给程序添加统一功能的技术。

其中有两层涵义:
*   第一:不修改源代码,即尽可能的解耦。
*   第二:添加统一的功能,即我们能实现的是添加统一的单一的功能,在某处使用AOP,
    我们只能实现一项单一的功能。如:日志记录。当然你可以添加多个AOP的模块到项目中,
    每一个实现不同功能,但是每一个功能必须是单一的。

AOP可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,
AOP可以说也是这种目标的一种实现。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,
提高程序的可重用性,同时提高了开发的效率。
主要功能
代码语言:javascript复制
日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要意图
代码语言:javascript复制
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,
我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AOP/OOP 之间的关系
代码语言:javascript复制
AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。
OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,
以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。

上面的陈述可能过于理论化,举个简单的例子,对于“雇员”这样一个业务实体进行封装,
自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。
而用AOP设计思想对“雇员”进行封装将无从谈起。

同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。

换而言之,OOD/OOP面向名词领域,AOP面向动词领域。

代码语言:javascript复制
很多人在初次接触 AOP 的时候可能会说,AOP 能做到的,一个定义良好的 OOP 的接口也一样能够做到,我想这个观点是值得商榷的。
AOP和定义良好的 OOP 的接口可以说都是用来解决并且实现需求中的横切问题的方法。但是对于 OOP 中的接口来说,
它仍然需要我们在相应的模块中去调用该接口中相关的方法,这是 OOP 所无法避免的,并且一旦接口不得不进行修改的时候,
所有事情会变得一团糟;AOP 则不会这样,你只需要修改相应的 Aspect,再重新编织(weave)即可。 
当然,AOP 也绝对不会代替 OOP。核心的需求仍然会由 OOP 来加以实现,
而 AOP 将会和 OOP 整合起来,以此之长,补彼之短。


当然,上述应用范例在没有使用AOP情况下,也得到了解决,例如JBoss 3.XXX也提供了上述应用功能,并且没有使用AOP。

但是,使用AOP可以让我们从一个更高的抽象概念来理解软件系统,AOP也许提供一种有价值的工具。可以这么说:

因为使用AOP结构,JBoss 4.0的源码要比JBoss 3.X容易理解多了,这对于一个大型复杂系统来说是非常重要的。

如果说面向对象编程是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话; 那么面向切面编程则是希望能够将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。

代码语言:javascript复制
面向切面编程是一个令人兴奋不已的新模式。就开发软件系统而言,它的影响力必将会和有着数十年应用历史的面向对象编程一样巨大。
面向切面编程和面向对象编程不但不是互相竞争的技术而且彼此还是很好的互补。
面向对象编程主要用于为同一对象层次的公用行为建模。它的弱点是将公共行为应用于多个无关对象模型之间。
而这恰恰是面向切面编程适合的地方。有了 AOP,我们可以定义交叉的关系,并将这些关系应用于跨模块的、彼此不同的对象模型。
AOP 同时还可以让我们层次化功能性而不是嵌入功能性,从而使得代码有更好的可读性和易于维护。它会和面向对象编程合作得很好。

二、个人理解中的AOP编程

代码语言:javascript复制
读到的这段话我感觉说的很清楚了:
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。
而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。
实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。
这样做的好处是降低了代码的复杂程度,使类可重用。      
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。
按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。
也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。   
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。
但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。
那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。       

一般而言,我们管切入到指定类指定方法的代码片段称为切面,
而切入到哪些类、哪些方法则叫切入点。
有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。
有了AOP,OOP变得立体了。  AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

举几个例子

代码语言:javascript复制
你的程序写好了。现在发现要针对所有业务操作添加一个日志,或者在前面加一道权限控制,怎么办呢?
传统的做法是,改造每个业务方法。这样势必把代码弄得一团糟。而且以后再扩展还是更乱。
aop的思想是引导你从另一个切面来看待和插入这些工作日志,不管加在哪,它其实都是属于日志系统这个角度的。权限控制也一样。
aop允许你以一种统一的方式在运行时期在想要的地方插入这些逻辑

          

如果有一个判断权限的需求,OOP的做法肯定是在每个操作前都加入权限判断。
那日志记录怎么办?在每个方法的开始结束的地方都加上日志记录。
AOP就是把这些重复的逻辑和操作,提取出来,运用动态代理,实现这些模块的解耦。
OOP和AOP不是互斥,而是相互配合。

一个具象化的理解

代码语言:javascript复制
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。
按照正常的逻辑,我们可以这么做。

第1版

代码语言:javascript复制
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,
提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。

第2版

代码语言:javascript复制
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。
于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。

第3版

代码语言:javascript复制
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。
红框处,就是面向切面编程。

三、iOS中如何实现AOP

代码语言:javascript复制
在iOS里面使用AOP进行编程,可以实现非侵入。不需要更改之前的代码逻辑,就能加入新的功能。
在iOS中实现AOP的核心技术是`Runtime`,使用`Runtime`的`Method Swizzling`黑魔法,
我们可以移花接木,在运行时将方法的具体实现添油加醋、偷梁换柱。
Aspects

Aspects是一个轻量级的面向切面编程的库。

它能允许你在每一个类和每一个实例中存在的方法里面加入任何代码。 可以在以下切入点插入代码: before(在原始的方法前执行) instead(替换原始的方法执行) after(在原始的方法后执行,默认)。 通过Runtime消息转发实现Hook。Aspects会自动的调用super方法,使用method swizzling起来会更加方便。

代码语言:javascript复制
这个库很稳定,目前用在数百款app上了。
它也是PSPDFKit的一部分,PSPDFKit是一个iOS 看PDF的framework库。作者最终决定把它开源出来。
安装:
代码语言:javascript复制
使用Aspects的项目要求是在RAC环境下,系统要求是iOS 7或更高,和OS X 10.7 及更高版本。
满足环境要求后,就可以使用Cocopods进行安装了。

在Podfile下添加:
    pod 'AspectsV1.4.2', '~> 1.4.2’

然后,命令行执行:
   pod install --verbose --no-repo-update

执行成功后,重新打开我们的项目目录下.xcworkspace文件。
主要方法:

Aspects为NSObject类提供了如下的方法:

代码语言:javascript复制
/**
 *   勾取某一对象的某一方法
 *   selector : 指定对象的方法
 *   options :枚举类型
 *   block :执行的自定义方法。在这个block里面添加我们要执行的代码。
 **/
   (id<AspectToken>)aspect_hookSelector:(SEL)selector
              withOptions:(AspectOptions)options
               usingBlock:(id)block
                    error:(NSError **)error;

 - (id<AspectToken>)aspect_hookSelector:(SEL)selector
              withOptions:(AspectOptions)options
               usingBlock:(id)block
                    error:(NSError **)error;

这两个方法,都可以用来修改原方法。
   ****  前一个创建的 全局切片
   ****  后一个创建的是专用切片
返回值是一个id<AspectToken>对象,可以用来注销在方法中所做的修改:

id<AspectToken> aspect = ...;
[aspect remove];

参数里面,第一个selector用来设置需要添加功能的方法,options有如下选择:

  AspectPositionAfter   = 0,            ///在原方法调用完成以后进行调用
  AspectPositionInstead = 1,            ///取代原方法                 
  AspectPositionBefore  = 2,            ///在原方法调用签执行   
  AspectOptionAutomaticRemoval = 1 << 3 ///在调用了一次后清除(只能在对象方法中使用)

block是用来写功能模块,参数可以为空,默认的第一个参数是id<AspectInfo>类型。AspectInfo协议有如下方法:

  /// 对象调用时,放回对象
  - (id)instance;
  /// 方法的原始实现
  - (NSInvocation *)originalInvocation;
  /// 原方法调用的参数
  - (NSArray *)arguments;
   最后error用于返回一个错误的指针。

官方的一个实用例子:

代码语言:javascript复制
[PSPDFDrawView aspect_hookSelector:@selector(shouldProcessTouches:withEvent:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, NSSet *touches, UIEvent *event) {
    // Call original implementation.
    BOOL processTouches;
    NSInvocation *invocation = info.originalInvocation;
    [invocation invoke];
    [invocation getReturnValue:&processTouches];

    if (processTouches) {
        processTouches = pspdf_stylusShouldProcessTouches(touches, event);
        [invocation setReturnValue:&processTouches];
    }
} error:NULL];

实际运用的一个例子

代码语言:javascript复制
#import <Aspects/Aspects.h>
#import <UIKit/UIKit.h>
@interface AOPHelper : NSObject
  (void) setup;
@end

@implementation AOPHelper
  (void)setup {
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^{
        NSLog(@"view did appear");
    } error:NULL];
}
@end


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [AOPHelper setup];
    return YES;
}

               类别 load 方法的形式的运用             

@implementation UIViewController (Logging)  (void)load
{
   [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                             withOptions:AspectPositionAfter
                              usingBlock:^(id<aspectinfo> aspectInfo) {        NSString *className = NSStringFromClass([[aspectInfo instance] class]);
       [Logging logWithEventName:className];
                              } error:NULL];
}

当然也可以使用自定义类并重写 load方法来实现功能。所以Aspectes的使用时机是: (1)AppDelegate中 (2)类别 load 方法的形式的运用 当然也可以写在工程中间。反正主要的原则就是在切面类调用之前执行 Aspectes 的方法。

其他:

Aspectes 会自动标记自己,所有很容易在调用栈中查看某个方法是否已经调用:

其中有 PSPDF前缀的断点都是Aspectes的断点。

PS:最后给大家推荐一个AOP的类库实现的用于界面上红点提醒的 MagicRemind 第三方类库。

小结

如果你对 AOP编程一无所知,或者一知半解,本文理论部分详细阐述了 AOP思想是什么,为什么需要AOP编程,本文可以说是目前为止对AOP阐述最深刻的文章之一了,下半部分的 Aspects 库的介绍是对 AOP实际使用而做的准备,相信本文对大家应该有所帮助。


参考文章:

  • iOS面向切面编程-AOP
  • iOS架构设计解耦的尝试之VC逻辑AOP切割
  • iOS 如何实现Aspect Oriented Programming (上)
  • iOS 如何实现Aspect Oriented Programming (下)

0 人点赞