面向切面编程
Q: 什么是食物?
A: 食物通常以碳水化合物、脂肪、蛋白质或水构成,能够借由进食或是饮用为人类或者生物提供营养或愉悦的物质。
食物的来源可以是植物、动物或者其他界的生物,例如真菌,亦或发酵产品像是酒精。
生物摄取食物后,被生物的细胞同化,提供能量,维持生命及刺激成长。
前段时间讨论到Java中的回调函数时,提到了面向切面(Aspect Oriented Programming)编程。它是一种程序设计思维,常常与面向对象(Object Oriented Programming)编程相比较,但是也常常被单独拎出来讨论。
OOP的诞生源自于人类与生俱来的分类抽象能力,回想一下,读小学的你,是否就能轻而易举地分辨“零食”和“正餐”的区别呢?零食美味而充满诱惑,但是对于你来说昂贵,而且吃太多存在挨骂的风险,相对的,正餐则略显的无聊和平淡,但是在你饿了的时候往往能让你的肚子不会咕咕叫。
于是自然而然你就可以将它们的共同点抽象出来,比如它们可以被食用,但是它们的味道、价格、食用时间乃至“副作用”之类的都不太一样。它们中相同的属性,就可以分配给一个只对这些相同的属性敏感的父元素。而不同的部分,就单独分配给父元素下属的不同的子元素。这样一来,子元素就不光含有自己的属性,也拥有了父元素的所有属性。
所以现在名词解释对你来说,会不会变得更容易呢?
面向切面
现在你知道了,事物中对于某些相同属性的抽象,构成了面向对象的基石。
来写一个简单的OOP的例子
// JavaScript ES6
class Person {
constructor(name) {
this.name = name;
}
greeting() {
console.log(`hello, my name is ${this.name}, `);
}
}
class Engineer extends Person {
constructor(name, level) {
super(name);
this.level = level;
}
// override
greeting() {
super.greeting();
console.log(`a ${this.level} Engineer.`);
}
}
class Architect extends Engineer {
constructor(name, level) {
super(name, level);
}
design(work) {
console.log(`right now, i\`m ${work}.`);
}
playGames(game, platform) {
console.log(`${this.name} is playing ${game} on ${platform} now.`);
}
}
let ted = new Architect('Ted', 'Junior');
ted.greeting();
ted.design('painting some blueprints');
>>>
hello, my name is Ted,
a Junior Engineer.
right now, i'm painting some blueprints.
侵入式切面
假设我们想要在对象 Person
的所有方法执行前,加入一段逻辑。我们有什么好办法呢?
代理函数是个很好的选择,我们在进行具体的业务调用时,直接调用一个代理函数就行了。
function before(person, fn, ...args) {
console.log('something running before');
return fn.apply(person, args);
}
函数 before
,将我们要调用的函数包裹起来,然后我们只需要这样调用这个代理函数,就能在其中的某个部分中添加逻辑。
before(ted, ted.design, 'painting some blueprints');
上面的全局函数,还可以直接添加进对象中,防止被不必要的类或者方法使用。
Person.prototype.before = function (fn, ...args) {
console.log('do sthing before');
return fn.call(this, args);
};
ted.before(ted.design, 'painting some blueprints');
虽然这种形式大体上能解决我们的需求,但是这种写法,仍然破坏了对象原有的调用形式。
// 原来的调用形式
ted.greeting();
ted.playingGames('DotA2', 'PC');
ted.design('painting some blueprints');
// 代理函数的调用形式
ted.before(ted.greeting);
ted.before(ted.playingGames, 'DotA2', 'PC');
ted.before(ted.design, 'painting some blueprints');
清一色的 before
函数,不仅写起来头晕,而且当你要改动的时候,所有调用这个代理函数的地方都要进行改动。这并不是我们想要的。
非侵入式切面
代理函数帮我们执行了函数,虽然能按照我们的期望执行程序,但我们更希望它直接返回函数给我们,这样我们就无须费劲心思地去到处修改旧代码,同时往代码中添加了新的功能。
function before(originTarget, fn) {
return function (...args) {
console.log('do sthing before');
return fn.apply(originTarget, args);
};
}
ted.playGames = before(ted, ted.playGames);
// 正常调用
ted.playGames('DotA2', 'PC');
以上代码还可以写成更加通用的写法
function before (clazz, fn) {
return function (...args) {
console.log(`now time: ${new Date()}`);
return clazz.prototype[fn].apply(this, args);
}
}
ted.greeting = before(Person, 'greeting');
// 正常调用
ted.greeting();
程序中那些无法通过父元素所聚合的共同属性,但是又切实地影响到了程序的写作——这种时候,切面编程就能发挥出它的用处。它将业务中的相同的部分抽象出来,组成一个个可拆卸的业务组件。面向对象通过继承和多态的纵轴让代码松耦合,而面向切面则是在业务并行展开的水平线上让代码松耦合。
中间件
提起AOP,总免不了让人想到Java Spring框架中的监听器、拦截器、过滤器。它们是典型的AOP编程思想的结晶——一条正常的程序流,被几个中间件拦截检查。
这启发了我们,在程序设计上的一条新思路:程序暴露出一段执行流,并且给开发者一把剪刀。它们可以随意在允许的范围内剪开程序,并织入想要执行的内容。最后这一段执行流合重新合并起来,收入程序的深处。
简单模拟一个中间件设计
// 切入点函数队列
let fns = [];
// 切点计数
let fnCounter = 0;
// 启动函数
function main() {
next();
}
// 执行下一个函数
function next() {
let fn = fns[fnCounter++]; // 取出函数数组里的下一个函数
if (!fn) { // 如果函数不存在,return
return;
}
fn(next); // 否则,执行下一个函数
}
// 将自定义的函数推入函数队列
function processer(fn) {
fns.push(fn);
}
function loginCheck(next) {
if (...)
next();
else
return;
}
function characterFilter(next) {
// doing some character filtering
next();
}
function mainService() {
// 主业务
console.log('some main services');
}
processer(characterFilter);
processer(loginCheck);
processer(mainService);
main(); // 模拟程序启动
原文地址: https://code.evink.me/2018/07/post/Aspect-Oriented-Programming/