AOP(Aspect-oriented programming,面向切面编程)

  • Post author:
  • Post category:其他


概述


面向切面的程序设计

(Aspect-oriented programming,AOP)是CS计算机科学中的一种程序设计泛型,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。其可以通过预编译方式和运行期动态代理实现在不修改源码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP可以说也是这种目标的一种实现。

通过在现有代码基础上增加额外的

通知

(Advice)机制,能够对被声明为“

切点

(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以 ‘set*’ 开头的方法添加后台日志”。

该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

面向切面的程序设计将代码逻辑切分为不同的模块(即

关注点

(Concern),一段特定的逻辑功能)。几乎所有的编程思想都涉及代码功能的分类,将各个关注点封装成独立的抽象模块(如函数、过程、模块、类以及方法等),后者又可供进一步实现、封装和重写。部分关注点“横切”程序代码中的数个模块,即在多个模块中都有出现,它们即被称作“

横切关注点

(Cross-cutting concerns, Horizontal concerns)”。

切面的概念源于对对象对象的改进,但并不只限于此,它还可以用来改进传统的函数。与切面相关的编程概念还包括

元对象协议

、主题(Subject)、

混入

(Mixin)和委托(Delegate)。




核心关注点

中分离出

横切关注点

是面向切面的程序设计的核心概念。分离关注点

使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过

切面

来封装、维护,这样原本分散在在整个应用程序中的变动就可以很好的管理起来。


  • 关注点

    (Concern):对软件工程有意义的小的、可管理的、可描述的软件组成部分,一个关注点通常只同一个特定概念或目标相关联。

  • 主关注点

    (Core concern):一个软件最主要的关注点。

  • 关注点分离

    (Separation of concerns,SOC):标识、封装和操纵只与特定概念、目标相关联的软件组成部分的能力,即标识、封装和操纵关注点的能力。

  • 方法

    (Method):用来描述、设计、实现一个给定关注点的软件构造单位。
  • 横切(Crosscut):两个关注点相互横切,如果实现它们的方法存在交集。

  • 支配性分解

    (Dominant decomposition):将软件分解成模块的主要方式。传统的程序设计语言是以一种线性的文本来描述软件的,只采用一种方式(比如:类)将软件分解成模块;这导致某些关注点比较好的被捕捉,容易进一步组合、扩展;但还有一些关注点没有被捕捉,弥散在整个软件内部。支配性分解一般是按主关注点进行模块分解的。

  • 横切关注点

    (Crosscutting concerns):在传统的程序设计语言中,除了主关注点可以被支配性分解方式捕捉以外,还有许多没有被支配性分解方式捕捉到的关注点,这些关注点的实现会弥散在整个软件内部,这时这些关注点同主关注点是横切的。

  • 切面

    (Aspect):在支配性分解的基础上,提供的一种辅助的模块化机制,这种新的模块化机制可以捕捉横切关注点。

日志功能即是横切关注点的一个典型案例,因为日志功能往往横跨系统中的每个业务模块,即“横切”所有有日志需求的类及方法体。而对于一个信用卡应用程序来说,存款、取款、帐单管理是它的核心关注点,日志和持久化将成为横切整个对象结构的横切关注点。

将日志记录,性能统计,安全控制,事务处理,

异常处理

等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。






运用

简而言之– AOP试图将每个业务功能分解为正交部分,这些部分称为安全性,日志记录,错误处理等方面。横切关注点的分离。看起来像:

AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

上面的陈述可能过于理论化,举个简单的例子,对于“雇员”这样一个业务实体进行封装,自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP设计思想对“雇员”进行封装将无从谈起。

同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。

换而言之,OOD/OOP面向名词领域,AOP面向动词领域。

C++ 应用

由于C ++ 11支持高阶函数,因此我们无需任何其他工具和框架即可实现分解,例如C#的PostSharp。

让我们从一个简单的方面开始-一个方面和一个功能。


每一个lambda都是独一无二的,即使参数是相同的类型。

//这是简单的lambda,其中包含琐碎的计算:
auto plus = [](int a, int b) { std::cout << a + b << std::endl; };

我想在计算之前和之后添加一些日志记录。我们不只是将这个样板代码添加到函数体中,而是走另一种方式。在C ++ 11中,我们可以编写高阶函数,该函数将函数作为参数并返回新函数作为结果:

//std::function 包装了一个返回值为void , 参数为Args... 的函数对象(安全函数指针)

template <typename ...Args> 
std::function<void(Args...)> wrapLog(std::function<void(Args...)> f) {
    return [f](Args... args){                                            //捕获函数对象f
        std::cout << "start"  << std::endl; 
        f(args...); 
        std::cout << "finish" << std::endl; 
    }; 
} 

在这里,我们使用  std :: function, 可变参数模板和lambda作为结果。当我希望实现最简单,最紧凑的解决方案时,我希望像这样使用它:

//将一个lambda表达式作为参数传入
auto loggedPlus = wrapLog(plus);

不幸的是,这将无法编译。“没有匹配的函数可以调用…。” 原因是lambda和std :: function之间的自动类型转换无法完成。当然,我们可以这样写来强制转换:

auto loggedPlus = wrapLog( static_cast<std::function<void(int,int)>> (plus) );

该行可以编译,但是很难看……我希望cpp委员会将解决此强制转换问题。同时,到目前为止,我发现的最佳解决方案是:

注:C ++ 11添加了非常有用的 type_traits库、

Functional C++



Thrill 中类型萃取



function_traits参考



boost库关于function_traits



typename显式声明



std::function::operator()

template <typename Function>
struct function_traits : public function_traits<decltype(&Function::operator())>
{

};

//将函数返回类型进行萃取 
template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>        //Args...可变模板参数 
{
    typedef ReturnType (*pointer)(Args...);
    typedef std::function<ReturnType(Args...)> function;
};
    
template <typename Function>
typename function_traits<Function>::function    //显式声明为类型名,而不是变量名
to_function (Function& lambda)
{
    return typename function_traits<Function>::function(lambda);
}

此代码


使用类型特征将匿名lambda转换为相同类型的std :: function


。我们可以这样使用它:

auto loggedPlus = wrapLog(to_function(plus));

注意:如果我们声明了不带可变参数模板的Aspect函数,则可以在不进行to_function()转换的情况下编写函数,但这会抵消编写进一步讨论的通用方面的好处。

附:

#include <iostream>
#include <functional>

//using namespace std;

/*
 *
 *  使用类型特征将匿名lambda转换为相同类型的std :: function
 *  原文地址:http://vitiy.info/c11-functional-decomposition-easy-way-to-do-aop/
 *
*/

template <typename ...Args>
std::function<void(Args...)> wrapLog(std::function<void(Args...)> f) {

	return [f](Args... args) {                                            //捕获函数对象f
		std::cout << "start" << std::endl;
		f(args...);
		std::cout << "finish" << std::endl;
	};
}

它是前向声明,声明这个 function_traits_base 结构 是一个 可变参数模板结构
template <typename> struct function_traits_base;										// 通例, 没有 后面的 特例 则会报错 主模板的声明中不允许使用模板参数列表

template <typename Function>
struct function_traits : public function_traits_base<decltype(&Function::operator())> {		
																						// https://segmentfault.com/q/1010000021360347/a-1020000021365367
};																						// 借助 function_traits 的辅助来获得其对应得 std::function 特化。

// lambda 转换为 std::function<void(Args...)> 需要知道 传入参数类型和返回值类型 ,这里进行萃取 
template <typename ClassType, typename ReturnType, typename... Args>		 // https://www.cnblogs.com/qicosmos/p/4325949.html
struct function_traits_base<ReturnType(ClassType::*)(Args...) const>		 // 特例化 :Args...可变模板参数 ,因为不知道lambda将传入几个参数
{																			 
	typedef ReturnType (*pointer)(Args...);									 // 暂时没有作用,可注释
	typedef std::function<ReturnType(Args...)> function;			
};

//template <typename> struct function_traits;
//
//template  <typename ClassType, typename ReturnType, typename... Args>
//struct function_traits<ReturnType(ClassType::*)(Args...) const>										//Args...可变模板参数 ,因为不知道lambda将传入几个参数
//{
//	typedef std::function<ReturnType(Args...)> function;
//};

template <typename Function>
typename function_traits<Function>::function	// 返回值类型 - function_traits<Function>::function , 作为 wrapLog 的 参数类型 std::function<void(Args...)> 
to_function(Function& lambda)					// 函数名 - to_function,传入参数 - lambda,参数类型 Function& = std::function<void( xx , xx )>
{
	return typename function_traits<Function>::function(lambda);	//将lambda传入function_traits<Function>::function,第24行
}

//functional里面有定义了个std::plus。如果把plus定义为global的,访问时在global查找定义就会和std::plus冲突,产生二义。
//auto plus = [](int a, int b) { std::cout << a + b << std::endl; };			//error,Plus不明确
std::function<void(int, int)> plus = [](int a, int b) { std::cout << a + b << std::endl; };

int main(int argc, char *argv[]) {
	//这是简单的lambda,其中包含琐碎的计算:
	//auto plus = [](int a, int b) { cout << a + b << endl; };

	//lambda和 std :: function 无法完成转换
	//auto loggedPlus = wrapLog(plus);												//test1

	//强制转换,OK,该行可以编译,但是很难看
	auto loggedPlus  = wrapLog(static_cast<std::function<void(int, int)>>(plus));   //test2
	loggedPlus(2, 3);

	//暂时最佳解决方案
	auto loggedPluss = wrapLog(to_function(plus));			//通过调用to_function函数 将 lambda表达式plus 转换为std::function<void(Args...)> 形式
	loggedPluss(2, 3);

	return 0;
}



第2部分–实际示例



简介已经结束,让我们在这里开始一些实际的编码。假设我们想通过ID在数据库中找到一些用户。在执行此操作的同时,我们还希望记录过程持续时间,检查请求方是否有权执行此类请求(安全检查),检查数据库请求是否失败,最后检查本地缓存以获取即时结果。


还有一件事–我不想为每种函数类型重写这些附加方面。因此,让我们使用可变参数模板编写它们,并获取尽可能通用的方法。


好的,让我们开始吧。我将为其他类(例如User等)创建一些虚拟实现。此类仅作为示例,实际的生产类可能完全不同,例如用户ID不应为int等。


示例用户类作为不可变数据:

// Simple immutable data 简单的不可变数据
class UserData {
public:
    const int id;
    const string name;
    UserData(int id, string name) : id(id), name(name) {}
};
    
// Shared pointer to immutable data
using User = std::shared_ptr<UserData>;

让我们将数据库模拟为简单的用户向量,并创建一种使用数据库的方法(通过ID查找用户):

// make <>这只是make_shared <>的快捷方式,没什么特别的。
vector<User> users {make<User>(1, "John"), make<User>(2, "Bob"), make<User>(3, "Max")};
 
auto findUser = [&users](int id) -> Maybe<User> {
    for (User user : users) {
        if (user->id == id)
            return user;
    }
    return nullptr;
};


Maybe<> monad


您可能已经注意到,请求函数的返回类型包含名为

Maybe <T>

的东西。本课程的灵感来自Haskell,


maybe monad


,其中还有一个主要的补充。

它不仅可以保存Nothing  状态和Content 状态,还可以包含Error 状态。


首先,这是Error 的样本类型:

/// Error type - int code + description
class Error {
public:
    Error(int code, string message) : code(code), message(message) {}
    Error(const Error& e) : code(e.code), message(e.message) {}
 
    const int code;
    const string message;
};

这是

Maybe

的简约实现:

template < typename T >
class Maybe {
private:
    const T data;
    const shared_ptr<Error> error;
public:
    Maybe(T data) : data(std::forward<T>(data)), error(nullptr) {}
    Maybe() : data(nullptr), error(nullptr) {}
    Maybe(decltype(nullptr) nothing) : data(nullptr), error(nullptr) {}
    Maybe(Error&& error) : data(nullptr), error(make_shared<Error>(error)) {}
        
    bool isEmpty() { return (data == nullptr); };
    bool hasError() { return (error != nullptr); };
    T operator()(){ return data; };
    shared_ptr<Error> getError(){ return error; };
};
    
template <class T>
Maybe<T> just(T t)
{
    return Maybe<T>(t);
}

请注意,您不必使用Maybe <>,这里仅作为示例。


在这里,我们还使用C ++ 11 中的nullptr具有它自己的类型这一事实。也许从该类型定义了构造函数,但不会产生任何状态。因此,当您从findUser函数返回结果时,无需显式转换为Maybe <> –您只需返回User或nullptr,就会调用适当的构造函数。


运算符()不做任何检查就返回可能的值,而getError()返回可能的错误。


函数just()用于显式Maybe <T>构造(这是标准名称)。


Logging aspect

日志

切面

首先,让我们重写日志切面,以便它将使用

std :: chrono

计算执行时间。另外,让我们添加新的字符串参数作为被调用函数的名称,该函数将打印到日志中。

template <typename R, typename ...Args>
std::function<R(Args...)> logged(string name, std::function<R(Args...)> f)
{
        return [f,name](Args... args){
           
            LOG << name << " start" << NL;
            auto start = std::chrono::high_resolution_clock::now();
            
            R result = f(std::forward<Args>(args)...);
            
            auto end = std::chrono::high_resolution_clock::now();
            auto total = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
            LOG << "Elapsed: " << total << "us" << NL;
            
            return result;
        };
}

注意

std :: forward

此处用于以更简洁的方式传递参数。我们不需要将返回类型指定为

Maybe <R>,

因为我们不需要执行任何特定的操作,例如在此处进行错误检查。


‘Try again’ aspect 切面

如果无法获取数据该怎么办(例如,在断开连接的情况下)。让我们创建一个切面,以确保在发生错误的情况下可以再次执行相同的查询。

// If there was error - try again
template <typename R, typename ...Args>
std::function<Maybe<R>(Args...)> triesTwice(std::function<Maybe<R>(Args...)> f)
{
        return [f](Args... args){
            Maybe<R> result = f(std::forward<Args>(args)...);
            if (result.hasError())
                return f(std::forward<Args>(args)...);
            return result;
        };
}


Maybe<>

用于识别错误状态。 此方法可以扩展–我们可以检查错误代码并确定执行第二个请求是否有意义(是网络问题还是数据库报告了某种格式错误)。


Cache aspect 缓存切面

接下来,让我们添加客户端缓存并在执行实际的服务器端请求之前检查它的内部空间(在功能世界中,这称为

备忘录

)。要在这里模拟缓存,我们可以使用std :: map:

map<int,User> userCache;

// Use local cache (memoize)
template <typename R, typename C, typename K, typename ...Args>
std::function<Maybe<R>(K,Args...)> cached(C & cache, std::function<Maybe<R>(K,Args...)> f)
{
        return [f,&cache](K key, Args... args){
            // get key as first argument
            
            if (cache.count(key) > 0)
                return just(cache[key]);
            else
            {
                Maybe<R> result = f(std::forward<K>(key), std::forward<Args>(args)...);
                if (!result.hasError())
                    cache.insert(std::pair<int, R>(key, result())); //add to cache
                return result;
            }
        };
}


如果不存在,此函数会将元素插入缓存。在这里,我们使用了缓存为std :: map的知识,但是可以将其更改为隐藏在某个接口后面的任何键值容器。


第二个重要部分,我们仅在这里使用第一个函数参数作为键。如果您有复杂的请求,其中所有参数都应充当组合键,该怎么办?这仍然是可能的,并且有很多方法可以做到。第一种方法只是使用

std :: tuple

作为键(请参见下文)。第二种方法是创建允许几个关键参数的缓存类。第三种方法是使用可变参数模板将参数组合到单个字符串缓存中。


使用


tuple


方法,我们可以这样重写它:

map<tuple<int>,User> userCache;

// Use local cache (memoize)
template <typename R, typename C, typename ...Args>
std::function<Maybe<R>(Args...)> cached(C & cache, std::function<Maybe<R>(Args...)> f)
{
        return [f,&cache](Args... args){
            
            // get key as tuple of arguments
            auto key = make_tuple(args...);
            
            if (cache.count(key) > 0)
                return just(cache[key]);
            else
            {
                Maybe<R> result = f(std::forward<Args>(args)...);
                if (!result.hasError())
                    cache.insert(std::pair<decltype(key), R>(key, result())); //add to cache
                return result;
            }
        };
}

现在,它变得更加通用。


Security aspect 安全切面

永远不要忘记安全性。让我们用一些虚拟类来模拟用户会话–

class Session {
public:
    bool isValid() { return true; }
} session;

安全检查高级功能将具有附加参数–会话。检查只会验证

isValid()

字段为true:

// Security checking
template <typename R, typename ...Args, typename S>
std::function<Maybe<R>(Args...)> secured(S session, std::function<Maybe<R>(Args...)> f)
{
        // if user is not valid - return nothing
        return [f, &session](Args... args) -> Maybe<R> {
            if (session.isValid())
                return f(std::forward<Args>(args)...);
            else
                return Error(403, "Forbidden");
        };
}


‘Not empty’ aspect

此示例中的最后一件事–让我们将未找到用户视为错误。

// Treat empty state as error
template <typename R, typename ...Args>
std::function<Maybe<R>(Args...)> notEmpty(std::function<Maybe<R>(Args...)> f)
{
        return [f](Args... args) -> Maybe<R> {
            Maybe<R> result = f(std::forward<Args>(args)...);
            if ((!result.hasError()) && (result.isEmpty()))
                return Error(404, "Not Found");
            return result;
        };
}

我在这里没有写关于错误处理方面的内容,但是也可以通过相同的方法来实现。请注意,在

Maybe <>

monad 内部使用错误传播,可以避免使用异常,并以不同的方式定义错误处理逻辑。


Multithread lock aspect 多线程锁切面

template <typename R, typename ...Args>
std::function<R(Args...)> locked(std::mutex& m, std::function<R(Args...)> f)
{
    return [f,&m](Args... args){
        std::unique_lock<std::mutex> lock(m);
        return f(std::forward<Args>(args)...);
    };
}

No comments.


FINALLY

最后,这一切 madness是什么原因?

对于这行:

// Aspect factorization

auto findUserFinal = secured(session, notEmpty( cached(userCache, triesTwice( logged("findUser", to_function(findUser))))));

Checking 检查(让我们查找ID为2的用户):

auto user = findUserFinal(2);
LOG << (user.hasError() ? user.getError()->message : user()->name) << NL;

// output:
// 2015-02-02 18:11:52.025 [83151:10571630] findUser start
// 2015-02-02 18:11:52.025 [83151:10571630] Elapsed: 0us
// 2015-02-02 18:11:52.025 [83151:10571630] Bob

好的,让我们为多个用户执行测试(在这里,我们将请求同一用户两次,并请求一个不存在的用户):

auto testUser = [&](int id) {
    auto user = findUserFinal(id);
    LOG << (user.hasError() ? "ERROR: " + user.getError()->message : "NAME:" + user()->name) << NL;
};

for_each_argument(testUser, 2, 30, 2, 1);

//2015-02-02 18:32:41.283 [83858:10583917] findUser start
//2015-02-02 18:32:41.284 [83858:10583917] Elapsed: 0us
//2015-02-02 18:32:41.284 [83858:10583917] NAME:Bob
//2015-02-02 18:32:41.284 [83858:10583917] findUser start
//2015-02-02 18:32:41.284 [83858:10583917] Elapsed: 0us
// error:
//2015-02-02 18:32:41.284 [83858:10583917] ERROR: Not Found
// from cache:
//2015-02-02 18:32:41.284 [83858:10583917] NAME:Bob
//2015-02-02 18:32:41.284 [83858:10583917] findUser start
//2015-02-02 18:32:41.284 [83858:10583917] Elapsed: 0us
//2015-02-02 18:32:41.284 [83858:10583917] NAME:John


如您所见,它按预期工作。显然,这种分解为我们带来了很多好处。分解导致

功能的解耦

,更多的模块化结构等。结果,您将更加关注实际的业务逻辑。


我们可以根据需要更改方面的顺序。当我们使方面函数变得相当通用时,我们可以重复使用它们,从而

避免了很多代码重复



除了使用函数,我们可以使用更复杂的函子(带有继承),并且可以使用

Maybe <>

代替更复杂的结构来保存一些其他信息。因此,整个方案是可扩展的。


另请注意,您可以将lambda作为附加的方面参数传递。


可以使用的工作示例:

github gist



ideone

Ps. BONUS:

template <class F, class... Args>
void for_each_argument(F f, Args&&... args) {
    (void)(int[]){(f(forward<Args>(args)), 0)...};
}

参考文章:


维基百科-面向切面的程序设计


Victor Laskin的博客


百度百科-AOP


Function Traits


boost库中function_traits