C++ 公共组件-轻量级AOP库
1.AOP介绍
AOP(Aspect-Oriented Programming,面向方面编程),可以解决面向对象编程中的一些问题,是OOP的一种有益补充。面向对象编程中的继承是一种从上而下的关系,不适合定义从左到右的横向关系,如果继承体系中的很多无关联的对象都有一些公共行为,这些公共行为可能分散在不同的组件、不同的对象之中,通过继承方式提取这些公共行为就不太合适了。使用AOP还有一种情况是为了提高程序的可维护性,AOP将程序的非核心逻辑都“横切”出来,将非核心逻辑和核心逻辑分离,使我们能集中精力在核心逻辑上,
在上图中,每个业务流程都有日志和权限验证的功能,还有可能增加新的功能,实际上我们只关心核心逻辑,其他的一些附加逻辑,如日志和权限不需要关注,这时,就可以将日志和权限等非核心逻辑“横切”出来,使核心逻辑尽可能保持简洁和清晰,方便维护。这样“横切”的另一个好处是,这些公共的非核心逻辑被提取到多个切面中了,使它们可以被其他组件或对象复用,消除了重复代码。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多处,而各处都基本相似,比如权限认证、日志、事务处理。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
2.AOP的简单实现-通过代理模式
实现AOP的技术分为:静态织入和动态织入。静态织入一般采用专门的语法创建“方面”,从而使编译器可以在编译期间织入有关“方面”的代码,AspectC++就是采用的这种方式。这种方式还需要专门的编译工具和语法,使用起来比较复杂。10.3节将要介绍的AOP框架正是基于动态织入的轻量级AOP框架。动态织入一般采用动态代理的方式,在运行期对方法进行拦截,将切面动态织入到方法中,可以通过代理模式来实现。下面看一个简单的例子,使用代理模式实现方法的拦截,如下面代码所示:
#include<memory>
#include<string>
#include<iostream>
using namespace std;
class IHello{
public:
IHello(){}
virtual ~IHello(){}
virtual void Output(const string& str){}
};
class Hello : public IHello{
public:
void Output(const string& str) override{
cout <<str<< endl;
}
};
class HelloProxy : public IHello{
public:
HelloProxy(IHello* p) : m_ptr(p){}
~HelloProxy(){
delete m_ptr;
m_ptr = nullptr;
}
void Output(const string& str) final{
cout <<"Before real Output"<< endl;
m_ptr->Output(str);
cout <<"After real Output"<< endl;
}
private:
IHello* m_ptr;
};
void TestProxy(){
std::shared_ptr<IHello> hello = std::make_shared<HelloProxy>(new Hello());
hello->Output("It is a test");
}
int main() {
TestProxy();
return 0;
}
输出结果如下:
Before real Output
It is a test
Before real Output
通过HelloProxy代理对象实现了对Output方法的拦截,这里Hello::Output就是核心逻辑,HelloProxy实际上就是一个切面,我们可以把一些非核心逻辑放到里面,比如在核心逻辑之前的一些校验,在核心逻辑执行之后的一些日志等。
虽然通过代理模式可以实现AOP,但是这种实现还存在一些不足之处:
·不够灵活,不能自由组合多个切面。代理对象是一个切面,这个切面依赖真实的对象,如果有多个切面,要灵活地组合多个切面就变得很困难。这一点可以通过装饰模式来改进,虽然可以解决问题但还是显得“笨重”。
·耦合性较强,每个切面必须从基类继承,并实现基类的接口。
我们希望能有一个耦合性低,又能灵活组合各种切面的动态织入的AOP框架。
3.轻量级的AOP框架的实现-通过模板实现
要实现灵活组合各种切面,一个比较好的方法是将切面作为模板的参数,这个参数是可变的,支持1到N(N>0)切面,先执行核心逻辑之前的切面逻辑,执行完之后再执行核心逻辑,然后执行核心逻辑之后的切面逻辑。这里,可以通过可变参数模板来支持切面的组合。AOP实现的关键是动态织入,实现技术就是拦截目标方法,只要拦截了目标方法,我们就可以在目标方法执行前后做一些非核心逻辑,通过继承方式来实现拦截,需要派生基类并实现基类接口,这使程序的耦合性增加了。为了降低耦合性,这里通过模板来做约束,即每个切面对象必须有Before(Args…)或After(Args…)方法,用来处理核心逻辑执行前后的非核心逻辑。下面介绍如何实现能灵活组合各种切面的动态织入的AOP框架,如下面代码所示:
-
Asepct.hpp
_Pragma("once")
#define HAS_MEMBER(member)\
template<typename T, typename... Args> \
struct has_member_##member\
{\
private:\
template<typename U> \
static auto Check(int) -> decltype(std::declval<U>().member(std::declval<Args>()...), std::true_type()); \
template<typename U> \
static std::false_type Check(...);\
public:\
enum{value = std::is_same<decltype(Check<T>(0)), std::true_type>::value};\
};\
HAS_MEMBER(Before)
HAS_MEMBER(After)
class NonCopyable{
public:
NonCopyable(const NonCopyable&) = delete; // deleted
NonCopyable& operator = (const NonCopyable&) = delete; // deleted
NonCopyable() = default; // available
};
template<typename Func, typename... Args>
struct Aspect : NonCopyable
{
Aspect(Func&& f) : m_func(std::forward<Func>(f))
{
}
template<typename T>
typename std::enable_if<has_member_Before<T, Args...>::value&&has_member_After<T, Args...>::value>::type Invoke(Args&&... args, T&& aspect)
{
aspect.Before(std::forward<Args>(args)...);//核心逻辑之前的切面逻辑
m_func(std::forward<Args>(args)...);//核心逻辑
aspect.After(std::forward<Args>(args)...);//核心逻辑之后的切面逻辑
}
template<typename T>
typename std::enable_if<has_member_Before<T, Args...>::value&&!has_member_After<T, Args...>::value>::type Invoke(Args&&... args, T&& aspect)
{
aspect.Before(std::forward<Args>(args)...);//核心逻辑之前的切面逻辑
m_func(std::forward<Args>(args)...);//核心逻辑
}
template<typename T>
typename std::enable_if<!has_member_Before<T, Args...>::value&&has_member_After<T, Args...>::value>::type Invoke(Args&&... args, T&& aspect)
{
m_func(std::forward<Args>(args)...);//核心逻辑
aspect.After(std::forward<Args>(args)...);//核心逻辑之后的切面逻辑
}
template<typename Head, typename... Tail>
void Invoke(Args&&... args, Head&&headAspect, Tail&&... tailAspect)
{
headAspect.Before(std::forward<Args>(args)...);
Invoke(std::forward<Args>(args)..., std::forward<Tail>(tailAspect)...);
headAspect.After(std::forward<Args>(args)...);
}
private:
Func m_func; //被织入的函数
};
//AOP的辅助函数,简化调用
template<typename... AP, typename... Args, typename Func>
void Invoke(Func&&f, Args&&... args){
Aspect<Func, Args...> asp(std::forward<Func>(f));
asp.Invoke(std::forward<Args>(args)..., AP()...);
}
- main.cpp
#include <iostream>
#include <functional>
#include "Aspect.hpp"
using namespace std;
struct AA{
void Before(int i){
cout <<"Before from AA"<<i<< endl;
}
void After(int i){
cout <<"After from AA"<<i<< endl;
}
};
struct BB{
void Before(int i){
cout <<"Before from BB"<<i<< endl;
}
void After(int i){
cout <<"After from BB"<<i<< endl;
}
};
struct CC{
void Before(){
cout <<"Before from CC"<< endl;
}
void After(){
cout <<"After from CC"<< endl;
}
};
struct DD{
void Before(){
cout <<"Before from DD"<< endl;
}
void After(){
cout <<"After from DD"<< endl;
}
};
void GT(){
cout <<"real GT function"<< endl;
}
void HT(int a){
cout <<"real HT function: "<<a<< endl;
}
void TestAop1(){
// 织入普通函数
std::function<void(int)> f = std::bind(&HT, std::placeholders::_1);
Invoke<AA, BB>(std::function<void(int)>(std::bind(&HT, std::placeholders::_1)), 1);
// 组合了两个切面AA BB
Invoke<AA, BB>(f, 1);
// 织入普通函数
Invoke<CC, DD>(>);
Invoke<AA, BB>(&HT, 1);
// 织入lambda表达式
Invoke<AA, BB>([](int i){}, 1);
Invoke<CC, DD>([]{});
}
/*------------------------------------*/
struct TimeElapsedAspect{
void Before(int i){
cout <<"time start"<< endl;
}
void After(int i){
cout <<"time end"<< endl;
}
};
struct LoggingAspect{
void Before(int i){
std::cout <<"entering"<< std::endl;
}
void After(int i){
std::cout <<"leaving"<< std::endl;
}
};
void foo(int a){
cout <<"real HT function: "<<a<< endl;
}
void TestAop2() {
Invoke<LoggingAspect, TimeElapsedAspect>(&foo, 1); //织入方法
cout <<"-----------------------"<< endl;
Invoke<TimeElapsedAspect, LoggingAspect>(&foo, 1);
}
int main() {
TestAop1();
TestAop2();
return 0;
}
实现思路很简单,将需要动态织入的函数保存起来,然后根据参数化的切面来执行Before(Args…)处理核心逻辑之前的一些非核心逻辑,在核心逻辑执行完之后,再执行After(Args…)来处理核心逻辑之后的一些非核心逻辑。上面代码中的has_member_Before和has_member_After这两个traits是为了让使用者用起来更灵活。使用者可以自由选择Before和After,可以仅仅有Before或After,也可以二者都有。
需要注意的是切面中的约束,因为通过模板参数化切面,要求切面必须有Before或After函数,这两个函数的入参必须和核心逻辑的函数入参保持一致,如果切面函数和核心逻辑函数入参不一致,则会报编译错误。从另外一个角度来说,也可以通过这个约束在编译期就检查到某个切面是否正确。
从测试结果中看到,我们可以任意组合切面,非常灵活,也不要求切面必须从某个基类派生,只要求切面具有Before或After函数即可(这两个函数的入参要和拦截的目标函数的入参相同)。