AOP(Aspect Oriented Program)面向切面编程 解析及示例

801 阅读3分钟

面向切面编程

这里先放上一段wiki上的定义:

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计剖面导向程序设计)是 计算机科学中的一种 程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

关注点分离

分离业务代码和数据统计代码(非业务代码),无论在什么语言中,都是AOP的经典应用之一。从核心关注点中分离出横切关注点,是 AOP 的核心概念。 简单来说,如果你需要在你的核心业务逻辑之前或之后插入一些非业务代码,就可以考虑使用AOP。 在前端的常见需求中,有以下一些业务可以使用 AOP 将其从核心关注点中分离出来 - Node.js 日志log - 埋点、数据上报 - 性能分析、统计函数执行时间 - 给ajax请求动态添加参数、动态改变函数参数 - 分离表单请求和验证 - 防抖与节流

基本实例

一、 before、after

AOP在不改变原函数内逻辑的基础上,在函数执行过程中插入非业务逻辑,由于不改变原函数逻辑,可以业务逻辑和非业务逻辑的低耦合。在JS中,AOP离不开高等函数,下面举几个简单的例子,为了简单起见,直接操作Function原型链来添加函数: 这个例子需要你对函数原型、this指向有一定了解

// AOP前置通知
Function.prototype._before = function(beforeFn) {
	let self = this;
	return function() {
		// 先执行before
		beforeFn.apply(self, arguments)
		// 再执行原函数
		return self.apply(self, arguments)
	}
}
// AOP后置通知
Function.prototype._after = function(afterFn) {
	let self = this
	return function() {
		let ret = self.apply(self, arguments)
		afterFn.apply(self, arguments)
		return ret
	}
}

这是一个基本的高阶函数,传入一个函数,构造一个新的函数,在新函数内保证原函数和传入切点函数的顺序执行,这样你可以在不影响原函数的同时,增加非业务逻辑

  • 统计函数执行时间 可以在函数执行前和函数执行后加入两个切面,用于计算执行时间 这样做对好处是不需要在原逻辑代码中加入非业务逻辑,后期如果要删除时间统计,直接删掉这段代码就可以,不会对原函数产生任何影响。如果把非业务逻辑写在主函数中,修改时就要小心翼翼的防止误改业务逻辑代码。
	let t1 , t2
	function doSth() {
		// 一些同步的复杂操作
		let i=0
		while(i < 10000) i ++
	}
	// 如果要删除统计时间功能,删掉下面这段代码就可以
	doSth = doSth._before(() => {
		t1 = +new Date()
		console.log('函数执行前')
	})._after(() => {
		t2 = +new Date()
		console.log(`函数执行完毕,耗时${t2 - t1}ms`)
	})

当然,我们需要考虑到目标函数是异步函数的情况,使用async/await 对_before和_after进行修改:

// AOP前置通知
Function.prototype._before = function (beforeFn) {
    let self = this;
    return async function () {
        // 先执行before
        beforeFn.apply(self, arguments);
        // 再执行原函数
        return await self.apply(self, arguments);
    };
};
// AOP后置通知
Function.prototype._after = function (afterFn) {
    let self = this;
    return async function () {
        let ret = await self.apply(self, arguments);
        afterFn.apply(self, arguments);
        return ret;
    };
};

// 异步函数
function doSthSync() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve();
            console.log("函数执行ing");
        }, 3000);
    });
}
// 前后切面
let t1, t2;
doSthSync = doSthSync
    ._before(() => {
        t1 = +new Date();
        console.log("函数执行之前");
    })
    ._after(() => {
        t2 = +new Date();
        console.log("函数执行完毕: 耗时" + (t2 - t1) + "ms");
    });

doSthSync();

打印结果:

743C19A5-6FDC-4F3E-B350-3636143A8FAA.png

这样就可以实现一个业务逻辑和非业务逻辑完全分离的函数执行时间统计的需求。

总结:使用高阶函数来封装原函数,在原函数外对函数功能进行扩展,以达到业务逻辑和非业务逻辑的解耦,这就是AOP给我们带来的好处

二、 around 环绕通知

实际上对统计时间这个需求来说,上面的实现方式还是略显复杂,时间统计算是一个功能点,却分散在_before和_after两个函数中调用,这种方式并不友好。 before和after分别在函数之前和函数之后插入切面,是否有一种方法可以同时在目标函数前后进行操作呢,around环绕通知可以满足这个需求

Function.prototype._around = function (aroundFn) {
    let self = this;
    function JoinPoint(args) {
        // 获取原函数控制权
        this.invoke = function () {
            self.apply(self, args);
        };
    }

    return function () {
        let args = [new JoinPoint(arguments)];
        aroundFn.apply(self, args);
    };
};

function doSth() {
    console.log("doSth");
}

doSth = doSth._around((descriptor) => {
    let fn = descriptor.invoke;
    let t1 = +new Date();
    // 执行原函数
    fn();
    console.log("函数执行时间:", +new Date() - t1);
});

doSth();

执行结果: [image:D30BA0A6-ABB3-41C8-8F81-0DE39A1FB78D-44166-0000FADA1FFB9B87/3ABAFE09-A449-436F-A944-BF09A6BA00F5.png]

  • 表单验证 分离提交和验证,解耦代码
	function validate() {
		// 验证
	}

	function submit() {}

	submit = submit._around((descriptor) => {
		let fn = descriptor.invoke
		if(validate()) {
			fn()
		} else {
			// 验证失败
		}
	})

	submit()

三、装饰器(Decorator)

装饰器是ES6中提出的一种方案,它的提出使AOP编程更加方便,可以说其设计思想本身就是面向切面的,JS 的装饰器可以用来“装饰”三种类型的对象:

  • 类的属性
  • 类的方法
  • 类本身 装饰器的语法很简单,只要在类的属性/方法、类本身前加上@Decorator即可,装饰器名字可以自定义,其本质是一个函数 这里介绍一下常用的类装饰器和方法装饰器
  • 装饰器介绍 类装饰器 类装饰器接受一个taget参数,其值是当前类的实例
// 类装饰器
@classDecorator
class Modal {}

// 定义装饰器
function classDecorator(target) {
    target.sayMyName = "我是通过装饰器赋值的名字!";
    return target; // 操作完后需要返回类本身
}

// 测试
console.log(Modal.sayMyName);

方法装饰器 类的方法装饰器接收三个参数target, name, descriptor

  1. target: 所装饰方法的类的原型,在这个例子里是 Modal.prototype
  2. name: 所装饰方法的名称,这个例子里是show
  3. descriptor:该方法的属性描述符,即: valuewritableenumerableconfigurable以及getset访问器,不了解的同学可以去温习一下Object.defineProperty这个函数
class Modal {
	@funcDecorator
	show() {
		console.log('弹窗被打开了!')
	}
}

function funcDecorator(target, name, descriptor) {
	const originalFn = descriptor.value  // 原函数的值
	const self = this  // 保留this引用
	descriptor.value = function() {
		console.log('要打开弹窗了!')
		return originalFn.apply(self, arguments)  // 要返回原函数的执行结果,保证外部调用逻辑不会被改变
	}
}

// 测试
const modal = new Modal()
modal.show()
// 打印:
// 要打开弹窗了!
// 弹窗被打开了!
  • 使用装饰器统计时间
// utils.js
export function measure(target, name, descriptor) {
	let oldFn = descriptor.value;
	descriptor.value = async function() {
            let startTime = Date.now()
            // 执行被装饰的函数
            let ret = await oldFn.apply(this, arguments)
            console.log(`函数${name}的执行时间为${Date.now() - startTime}`)
            return ret
	}
}

定义好装饰器之后,只需要在需要进行时间统计的类中引用:

import {measure} from './utils.js'

class Prod {
	...
 // 装饰器
	@measure
	showDialog() {
		// 业务逻辑
	}
	...
}
  • 定义全局类错误处理装饰器 只放上代码,自行理解
	// 全局类错误处理装饰器
const asyncClass = (errHandler?: (err: Error) => void) => (target: any) => {
  Object.getOwnPropertyNames(target.prototype).forEach((key) => {
    let fn = target.prototype[key];
    target.prototype[key] = async (...args: any) => {
      try {
        await fn.apply(this, ...args);
      } catch (err) {
        errHandler && errHandler(err);
      }
    };
  });
  return target;
};

使用:

// 对class B中所有方法做异常处理,出现异常时打印err
@asyncClass((err: Error) => {
  console.log('全局拦截:', err);
})
class B {
  someFunc() {
    throw new Error("一个错误");
  }
}

打印结果:

F7B5CA26-7C6B-45C6-9BEC-D4F380004C23.png