面向方面编程简介

什么是面向方面编程(What)

面向侧面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。

侧面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与侧面相关的编程概念还包括元对象协议、主题(subject)、混入(mixin)和委托。

基本概念

  • 关注点(concern):对软件工程有意义的小的、可管理的、可描述的软件组成部分,一个关注点通常只同一个特定概念或目标相关联。
  • 主关注点(core concern):一个软件最主要的关注点。
  • 关注点分离(separation of concerns,SOC):标识、封装和操纵只与特定概念、目标相关联的软件组成部分的能力,即标识、封装和操纵关注点的能力。
  • 方法(method):用来描述、设计、实现一个给定关注点的软件构造单位。
  • 横切(crosscut):两个关注点相互横切,如果实现它们的方法存在交集。
  • 支配性分解(dominant decomposition):将软件分解成模块的主要方式。传统的程序设计语言是以一种线性的文本来描述软件的,只采用一种方式(比如:类)将软件分解成模块;这导致某些关注点比较好的被捕捉,容易进一步组合、扩展;但还有一些关注点没有被捕捉,弥散在整个软件内部。支配性分解一般是按主关注点进行模块分解的。
  • 横切关注点(crosscutting concerns):在传统的程序设计语言中,除了主关注点可以被支配性分解方式捕捉以外,还有许多没有被支配性分解方式捕捉到的关注点,这些关注点的实现会弥散在整个软件内部,这时这些关注点同主关注点是横切的。
  • 侧面(aspect):在支配性分解的基础上,提供的一种辅助的模块化机制,这种新的模块化机制可以捕捉横切关注点。

主关注点中分离出横切关注点是面向侧面的程序设计的核心概念。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过侧面来封装、维护,这样原本分散在在整个应用程序中的变动就可以很好的管理起来。

对于一个信用卡应用程序来说,存款、取款、帐单管理是它的主关注点,日志和持久化将成为横切整个对象结构的横切关注点。

为什么需要面向方面编程(Why)

AOP技术的优势是显而易见的。在面向对象的世界里,人们提出了各种方法和设计原则来保障系统的可复用性与可扩展性,以期建立一个松散耦合、便于扩展的软件系统。例如GOF提出的“设计模式”,为我们提供了设计的典范与准则。设计模式通过最大程度的利用面向对象的特性,诸如利用继承、多态,对责任进行分离、对依赖进行倒置,面向抽象,面向接口,最终设计出灵活、可扩展、可重用的类库、组件,乃至于整个系统的架构。在设计的过程中,通过各种模式体现对象的行为、暴露的接口、对象间关系、以及对象分别在不同层次中表现出来的形态。然而鉴于对象封装的特殊性,“设计模式”的触角始终在接口与抽象中大做文章,而对于对象内部则无能为力。

通过“横切”技术,AOP技术就能深入到对象内部翻云覆雨,截取方法之间传递的消息为我所用。由于将核心关注点与横切关注点完全隔离,使得我们能够独立的对“方面”编程。它允许开发者动态地修改静态的OO模型,构造出一个能够不断增长以满足新增需求的系统,就象现实世界中的对象会在其生命周期中不断改变自身,应用程序也可以在发展中拥有新的功能。

设计软件系统时应用AOP技术,其优势在于:

  • 在定义应用程序对某种服务(例如日志)的所有需求的时候。通过识别关注点,使得该服务能够被更好的定义,更好的被编写代码,并获得更多的功能。这种方式还能够处理在代码涉及到多个功能的时候所出现的问题,例如改变某一个功能可能会影响到其它的功能,在AOP中把这样的麻烦称之为“纠结(tangling)”。
  • 利用AOP技术对离散的方面进行的分析将有助于为开发团队指定一位精于该项工作的专家。负责这项工作的最佳人选将可以有效利用自己的相关技能和经验。
  • 持久性。标准的面向对象的项目开发中,不同的开发人员通常会为某项服务编写相同的代码,例如日志记录。随后他们会在自己的实施中分别对日志进行处理以满足不同单个对象的需求。而通过创建一段单独的代码片段,AOP提供了解决这一问题的持久简单的方案,这一方案强调了未来功能的重用性和易维护性:不需要在整个应用程序中一遍遍重新编写日志代码,AOP使得仅仅编写日志方面(logging aspect)成为可能,并且可以在这之上为整个应用程序提供新的功能。

总而言之,AOP技术的优势使得需要编写的代码量大大缩减,节省了时间,控制了开发成本。同时也使得开发人员可以集中关注于系统的核心商业逻辑。此外,它更利于创建松散耦合、可复用与可扩展的大型软件系统。

如何实现面向方面编程(How)

使用js实现before(前置通知)、after(后置通知)、around(环绕通知)。

before(前置通知)

before函数,用来实现函数的前置通知。在目标函数的前面执行一些前置操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// AOP 前置通知函数声明
/**
* 给方法加入前置切片函数
* 可以在执行方法之前执行一些操作,
* 前置切片的返回值为false时,不影响原方法的执行
* @param func {Function} 被前置执行的函数
* @return {Function} 加入前置通知的函数
*/
Function.prototype._before = function(func){
var __self = this;
return function(){
func.apply(__self, arguments);
return __self.apply(__self, arguments);
}
}

// 代码
function a(){
console.log('I\'m a');
}

a = a._before(function(){
console.log('before');
});

a();
// 结果:
// before
// I'm a

after(后置通知)

after函数,用来实现函数的后置通知。在目标函数的后面面执行一些后置操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// AOP 后置通知函数声明
/**
* 给方法加入后置切片函数
* 可以在执行方法之之后执行一些操作
* 后置切片的返回值为false时,不影响原方法的执行
* @param func {Function} 被后置执行的函数
* @return {Function} 加入后置通知的函数
* @constructor
*/
Function.prototype._after = function(func){
var __self = this;
return function(){
var ret = __self.apply(__self, arguments);
func.apply(__self, arguments);
return ret;
}
}

// 代码
function b(){
console.log('I\'m b');
}

b = b._after(function(){
console.log('after');
});

b();
// 结果:
// I'm b
// after

around(环绕通知)

在around函数中,引入了一个JoinPoint对象。JoinPoint对象封装了目标函数和目标函数的参数。在调用JoinPoint对象的invoke函数时,会去调用原来的目标函数。在调用invoke时,如果需要改变目标函数的this对象,需要将对象传入到invoke的参数中。around函数,可以在目标函数的前面和后面随意加入逻辑代码,也可以根据条件判断是否执行目标函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// AOP 环绕通知函数声明
/**
* 切入点对象
* 不允许切入对象多次调用
* @param obj 对象
* @param args 参数
* @constructor
*/
function JoinPoint(obj, args){
var isapply = false; // 判断是否执行过目标函数
var result = null; // 保存目标函数的执行结果

this.source = obj; // 目标函数对象
this.args = args; // 目标函数对象传入的参数

/**
* 目标函数的代理执行函数
* 如果被调用过,不能重复调用
* @return {object} 目标函数的返回结果
*/
this.invoke = function(thiz){
if(isapply){ return; }
isapply = true;
result = this.source.apply(thiz || this.source, this.args);
return result;
};

// 获取目标函数执行结果
this.getResult = function(){
return result;
}
}

/**
* 方法环绕通知
* 原方法的执行需在环绕通知方法中执行
* @param func {Function} 环绕通知的函数
* 程序会往func中传入一个JoinPoint(切入点)对象, 在适当的时机
* 执行JoinPoint对象的invoke函数,调用目标函数
*
* @return {Function} 切入环绕通知后的函数,
*/
Function.prototype._around = function(func){
var __self = this;
return function(){
var args = [new JoinPoint(__self, arguments)];
return func.apply(this, args);
}
}

// 代码

var isAdmin = true;

function c(){
console.log('show user list');
}

c = c._around(function(joinpoint){
if(isAdmin){ // 满足条件时,执行目标函数
console.log('is admin');
joinpoint.invoke(this);
}
});

c();
// 结果
// if isAdmin == true
// is admin
// show user list
// if isAdmin == false

参考链接

  1. 面向侧面的程序设计,by wikipedia.
  2. 什么是面向切面编程AOP?,by 知乎.
  3. 什么是面向方面编程,by liuweitoo.
  4. AOP面向方面编程,by 规速.
  5. 团队开发框架实战—面向切面的编程 AOP,by Bobby0322.
  6. 轻松理解AOP(面向切面编程),by -望远-.
  7. AOP在JS中的实现及应用,by _Sirius.