AOP,Aspect Oriented Programming,面向切片(面)编程,其实就是一种函数式编程的衍生范型。从名字上其实还是比较容易理解的,如果说分层是从横向切面,那么AOP则是面向纵向切面,也就是说,每个大的功能模块,可以纵向切成多个片(面),在这个片里可以动态的增加处理功能,它可以通过代理接口或者预编译的方式来实现。通过业务逻辑的隔离实现解耦合。
举一个简单的例子,如果有几组类似的功能类,比如Array0中有三个类,Array1中有四个类,而Array2中有五个类(…)。这几个类都需要在进入前进行一个特定的处理而这个处理每组是相同的,组之间的处理是相似的。那么就可以对每组进行切片,然后抽象切片,完成对这些组及组内类的处理。
有过Java中Spring编程的经验的同学应该非常容易明白上面说的意思。面向切片编程有几个概念需要了解:
1、Aspect:其实就是面,在编程里可以看做类,它包含一些Pointcut和Advice
2、Pointcut:一组Join Point,即Advice发生的地方
3、Join Point:可以理解成接口调用点
4、Advice:可以理解成Join Point动作的内容,例如Spring中的before,after等前后执行的相关代码
其它还有一些概念,如target,weaving,introduction,proxy等,大家可以参考相关书籍和资料。
面向切片和面向对象以及其它什么面向范式,都有着具体的应用场景,还是那句话,包打天下的打手是不存在的。不要在这些无意义的互相替换或者哪个更优上浪费生命。其实无论哪种编程,站到思想的高度来看,其实都是一种思想的解放或者说认知的扩展,这才是最重要的。面向切片有可能和一些设计模式有些类似,但这无关紧要,本身设计模式也是一种设计思想的体现。面向切片更注重的是切出共同的行为形成一种接口可共用,这才是重点。所有的思想设计最终都是为了让软件的设计、开发更符合原则。
面向切片编程可能更注重的是各个模块对象间的抽象的关系特别是动作的关系,也可以理解成纵向的分层。在这一点上,和DDD的设计原则在业务逻辑上的处理有一种相通的意思。
在前面的模板编程里,提到了一个耗时处理的模板函数,其实就是面向切片编程的方式。在不同的函数和不同的类中,可能都需要这个耗时函数,那么最简单的方法就是抽象出一个时间函数,在类或者函数中调用。但是这样的调用,往往会引入重复的代码,这时就可以考虑切片编程了。看下面的例子:
auto TestTime(int a,int b)
{
struct timeval t1, t2;
double timeuse;
gettimeofday(&t1, NULL);
//todo:work time
gettimeofday(&t2, NULL);
timeuse = t2.tv_sec - t1.tv_sec + (t2.tv_usec - t1.tv_usec) / 1000000.0;
printf("Use Time:%f\n", timeuse);
}
这是一个很常见的计算耗时的方法,如果每个函数或者类对象中都有这么个计算过程,大量的重复代码和复杂的维护成本都让人想想就难受。那么能不能按照上面的AOP原则进行切片呢?首先要抽象的是公共的方法,都是要计时,计时又需要两部分,即动态的处理需要计时的函数,以及另外一个计时的过程。好,这样就可以先抽象第一步,即把计时的函数进行控制,这就用到了以前那个计时处理的函数:
template <typename Fn, typename... Args>
void TestTime(Fn &fn, Args... args)
{
struct timeval t1, t2;
double timeuse;
gettimeofday(&t1, NULL);
fn(std::forward<Args>(args)...);
gettimeofday(&t2, NULL);
timeuse = t2.tv_sec - t1.tv_sec + (t2.tv_usec - t1.tv_usec) / 1000000.0;
printf("Use Time:%f\n", timeuse);
}
第二步,可不可以把gettimeofday这种重复的代码封装起来。这时候儿很容易想到什么 ?RAII,C++中对付这种最简单的最直接的方式。时间的计算以构造函数开始,以析构函数结束,恰恰是目前所需要的,看下面的代码:
#include <cstddef>
#include <stdio.h>
#include <sys/time.h>
class CaclTime {
public:
CaclTime() { gettimeofday(&tStart_, NULL); };
~CaclTime() {
gettimeofday(&tEnd_, NULL);
timeuse_ = tEnd_.tv_sec - tStart_.tv_sec + (tEnd_.tv_usec - tStart_.tv_usec) / 1000000.0;
printf("Use Time:%f\n", timeuse_);
}
private:
struct timeval tStart_;
struct timeval tEnd_;
double timeuse_ = 0.0;
};
template <typename Fn, typename... Args>
void TestTime(Fn &fn, Args... args)
{
CaclTime();
fn(std::forward<Args>(args)...);
}
把这两个点搞定,再往上层抽象其实就简单了。主要考虑有三类:
1、切片功能类和其进一步的抽象类,见上面的类
2、业务类,即谁调用切片功能类
3、抽象代理类,即把上面的计算通过代理来实现进一步的抽象
void Run(int num) {
for (int i = 0; i < num; i++) {
// todo
}
}
template <typename T> struct TimeProxy {
public:
template <typename F, typename... Args> static decltype(auto) Task(F f, Args &&...args) {
T t;
return f(std::forward<Args>(args)...);
}
};
TimeProxy<CaclTime>::Task(Run, 100);//注意,实际使用中必须在模板实例化后才可使用,比如函数中,否则报错“requires template<> synax”
而为了处理多个切片,可以抽象代理类:
//利用变参模板和tuple的特性来获取指定的类型
template<typename ...Types>
class ProxyObjectTypes {
public:
template<std::size_t ID>
struct GetType {
using Type = typename std::tuple_element<ID, std::tuple<Types...>>::type;
};
};
那么下来就可以通过Tuple来动态获取类型并展开:
template <typename... Types> struct ProxyTypes<ProxyObjectTypes<Types...>> {
template <typename F, typename... Args> static decltype(auto) Process(F &&f, Args &&...args) {
size_t num = sizeof...(Types);
for (int i = 0; i < num; i++) {
using Tfunc = typename ProxyObjectTypes<Types...>::template GetType<i>::Type;
Tfunc tf;
}
return std::forward<F>(f)(std::forward<Args>(args)...);
}
};
可是这个不符合现在在编译期展开的小潮流,那么用什么呢?std::make_index_sequence来处理:
template <typename... Types> struct ProxyTypes<ProxyObjectTypes<Types...>> {
template <typename F, typename... Args> static decltype(auto) Process(F &&f, Args &&...args) {
return Call(std::make_index_sequence<sizeof...(Types)>{}, std::forward<F>(f), std::forward<Args>(args)...);
}
template <std::size_t ID, typename F, typename... Args>
static decltype(auto) Call(std::index_sequence<ID>, F &&f, Args &&...args) {
using Tfunc = typename ProxyObjectTypes<Types...>::template GetType<ID>::Type;
Tfunc tf;
return std::forward<F>(f)(std::forward<Args>(args)...);
}
template <std::size_t ID, std::size_t... R, typename F, typename... Args>
static decltype(auto) Call(std::index_sequence<ID, R...>, F &&f, Args &&...args) {
using Tfunc = typename ProxyObjectTypes<Types...>::template GetType<ID>::Type;
Tfunc tf;
return Call(std::index_sequence<R...>{}, std::forward<F>(f), std::forward<Args>(args)...);
}
};
当然,在最后展开时,也可以使用前面学习过的std::initializer_list和逗号展开方法,不过这样做需要重新写一下上面的代码,有兴趣可以自己搞一下。上面的代码虽然可以使用,但是如果在上面的抽象中再增加一些SFINAE的控制或者C++20 的concepts的处理,就更安全了。
最后,使用这种切片编程,需要关注性能的要求,到达一定程度后,可能就需要改变方式来处理。如果想进一步的抽象封装还可以通过基础的宏定义来完成,这些都不难,有兴趣可以自己搞一搞。另外这个代码整体的流程都没有支持类成员变量,如果想支持只需要增加相关的参数和设置即可。
所有的设计思想和编程模式,都是一种抽象。既然是一种抽象,就意味着从思想的高度来看问题,不能纠结于具体的某个实现细节。思想是一种指导行为的原则,而不是指导行为的行动过程。很多人往往忽视了这一点。所以在对待编程思想和设计思想上,会有两种论调,一种是无用论,不管是看得懂还是装看不懂,觉得这种思想没啥实际作用,不如实际写点代码有用;另外一种是空谈论,坐而论道,不顾实际,觉得写代码特别LOW。正如前面反复强调的,计算机知识是一种高度的理论和实践相结合的知识,理论和实践是一种互相促进互相制约的一种状态 。
在计算机技术上,知行合一,更体现的淋漓尽致。