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实现
先上一段原始代码,可将其放大为我们开发中的任意功能模块:1
2
3var dosome = function(v){ //功能代码
console.log(`----------${v}----------`);
}
此时,我们想这个函数在每次使用之前添加使用日志,最常见的情况就是将日志模块的代码嵌入到dosome方法中
这种方式的弊端有以下:
- 如果同时对多个方法添加日志,此时就需要在每个方法中添加代码
- 对原代码具有破坏性,为dosome方法硬加上了不属于它的日志代码
- 不够灵活,修改时可能需要对每个方法进行检查
可能会有朋友会说,我们将日志模块封装好后只需要一句代码就可以很方便达到目的,为什么还要用AOP的方式。
请记住 不破坏原对象本身
面向切片完成的是 不改变原对象而达到扩展功能 的目的。
现在,我们在dosome基础上,用AOP的方式实现在每次调用dosome方法前打印一段话1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var 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方法成功添加前置切面,接下来我们实现后置切片就很简单了, 注意:此方式有致命缺点
1 | var addAOPafter = function(fn,afn){ //后置切片 |
现在来将addAOP的方法进行封装,封装后实现在一个方法中实现前后切片的添加。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//双切
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编程的一种有益补充
另:本文之所以称为常规版,是因尚未完成异步状态下后置切片,敬待后续~