[转]什么是AOP?2007年08月10日 09:36 A.M.原文引用:http://blog.csdn.net/vargas/archive/2007/01/16/1485052.aspx
邓辉
软件开发领域一直一来的一个核心问题就是如何能够更好地满足Dijkstra所提出的separation of concerns原则。这个原则表达了代码和开发过程的一个最为重要的特性,但是遗憾的是它只是一个原则,其中并没有告诉我们如何做才能满足这一原则。人们在寻求能够满足这一原则的实现技术上进行了很多的探索,也取得了许多成就。其中,AOP(Aspect-Oriented Programming)就是这方面的一项最新技术。AOP的概念最初是由Xerox PARC研究中心的研究人员首先提出的,其目标是通过提供一些方法和技术,把问题领域分解成一系列的functional component和一系列横跨多个functional component的aspect,然后组合这些component和aspect,获得系统的实现。
在AOP提出很长一段时间内,基本上都处于一种学术研究和试验阶段。不过,近几年来,情况发生了改变。由于企业应用复杂度的不断提高,对软件开发技术提出了新的挑战。如何才能使得应用开发者仅仅关注于业务逻辑本身的开发,而不用纠缠于那些诸如安全、事务、日志等和业务逻辑无关但又是系统有效地执行业务逻辑所必须的功能呢?如果有某种技术能够把这些与具体业务逻辑无关但又是每个应用系统不可缺少的功能块,和业务逻辑做到干净的隔离,并且又能够通过一些简单的配置或者“胶水”代码,在需要时“织入”到应用系统中,那么不仅会大大提高开发效率,有效地降低软件的复杂性,而且在代码的清晰性、模块化、可测试性方面也会取得很好的提升。这正是AOP所要解决的问题,下面我将从系统分析、设计活动最为基本的目标入手,介绍一下AOP到底是什么,它解决了我们所面临的哪些困难。
AOP是什么?
在我们进行软件开发时,最为理想的情况应该是:每个需求概念元素都能够在源代码中有一个直接、清晰的表示。如果我们能够做到这一点,那么就能够在根本上把变化造成的影响局部化,而这正好是团队高效地的并行开发、架构顺畅地演化以及有效达成迭代的基础。
举例来说,在我们项目所开发的接入网软件系统中,PSTN用户端口这个概念就直接对应于程序中的一个设计表示:PstnUser类。V5链路这个概念也直接对应于一个设计表示:Link类。下面我们来考虑这样一个需求:由于电信级的设备都有较高的可靠性要求,所以系统必须要提供主备的功能。为了能够实现主备功能,除了硬件方面的一些要求外,在软件方面我们还必须要做到当一个业务对象的状态发生变化时,我们必须要把该状态及时地同步到备机以保证在主备切换后,系统还能够提供正常的业务功能。可以看出这是一个单一的需求描述,但是就是这样一个简单的单一功能需求却很难被映射到一个单一的设计表示结构上面。一般来说,我们会在业务对象中状态需要同步的地方都放上触发同步的逻辑。这种实现方法背离了需求和实现间一一映射的目标,相反这是一种需求到实现间的一对多的映射关系。
那些在设计中所捕捉到的需求元素,一般来说很可能会随着软件开发的进展、程序的演化而发生变化。如果在我们的程序中做到了需求元素和实现结构间的一一映射,那么这个实现结构就会非常易于修改和维护。相反,如果是一个一到多映射,那么在进行修改或者维护时就会非常得困难,因为在实现结构中有多个地方都得被更新,并且还得保证这些更新是完全且一致的。如果遗漏掉一个地方,或者更新时没有做到一致,那么就会造成一些非常微妙的bug,定位出这些bug很可能会花费你不少时间。
现在,我们换一个角度来看看这个问题。我们希望每个需求概念元素都能清晰地映射到一个实现结构上。我们同样也希望每个实现结构能够清晰地映射到唯一的一个需求概念元素上。但是,在上面所列举的系统主备的例子中,我们看到了业务对象状态同步这个需求概念元素散布在实现当中,这也意味着在业务对象实现中,不得不处理多种需求元素。对于PstnUser来说,它现在得处理至少两种需求概念:1、实现PSTN用户端口本身的逻辑;2、在需要时进行业务状态的同步。这是一种典型的需求概念元素和实现结构间的多对一的映射关系,敏锐的读者一定会发现这种情况违反了SRP(Single Responsibility Principle)。
这种多对一的情况同样也会带来很多问题。因为在一个类中,除了要实现本身要表示的那个需求概念外,还不得不处理很多和这个需求概念毫无关系的其他要素,比如:同步、事务、安全以及持久化等。这些和该类毫无关系的元素不但使得该类具有多个变化的原因,同时也对该类造成了很大的侵入性,使得该类极难重用、测试。
一般来说,我们在真正的软件开发中所遇到的真实情况更加复杂,我们基本上不可能遇到理想的概念和实现间的一一映射,而是多对多的映射。这也是导致软件如此难以维护、难以理解、如此复杂的原因。虽然面向对象方法在解决这个问题上取得了不小的进步,但是在日益复杂的问题领域面前,它还无法使我们完全做到需求概念元素和实现结构之间清晰、干净的映射关系。而这正是Aspect-Oriented Programming试图要解决的问题。通过AOP,我们在实现一一映射这个目标上又迈进了一大步。在AOP中引入了一个新的构造元素aspect,通过它我们可以达成在OOP中无法做到的需求概念元素和实现结构间的一一映射,比如:前面提到的系统业务状态同步需求。
请记住,我们所追求的就是需求概念元素和实现结构之间的一一映射。无论何时,无论何种原因,只要背离了这个目标,我们都会遇到麻烦。Structural Programming、Object-Based Programming、Object-Oriented Programming、Functional programming、Generic Programming以及Aspect-Oriented Programming这些编程范型都是为了在某个纬度上帮助我们更近地达成这个目标。
案例研究
一般来说,完全的AOP开发是需要开发环境支持的,比如:AspectJ、Spring或者JBOSS。对于C++语言来说,到目前为止还没有一个经过实践检验的支持AOP的开发环境出现,更不用说嵌入式系统了。不过,我们还是可以通过一些惯用的技术来手工地达成AOP的效果。在本小节中,我会通过项目中的一个实例来介绍一下比较常用的两种技术。我还以上面提到过的PstnUser对象状态同步为例进行介绍。传统实现方法的代码片断如下:
class PstnSynAgent
{
// . . .
public:
static void AskForSynHookOn(V5IID, L3ADDR);
static void AskForSynHookOff(V5IID, L3ADDR)
// . . .
};
void PstnUser::HookOn()
{
// do something corresponding to HookOn Event.
. . .
PstnSynAgent::AskForSynHookOn(GetV5IID(), GetL3Addr() );
}
void PstnUser::HookOff()
{
// do something corresponding to HookOff Event.
. . .
PstnSynAgent::AskForSynHookOff(GetV5IID(), GetL3Addr() );
}
可以看出,上面的代码片断中存在我们在前面所提到的问题,同步逻辑散布在PstnUser对象中那些需要同步状态的方法中。而PstnUser对象本身也违反了SRP,它不但要完成PstnUser本身的逻辑,还得去完成同步触发逻辑。下面我们将通过两种惯用的技术手法来达成AOP以消除上述味道,这两种方法是:Decorator模式以及Policy-Based Design。
1) 使用Decorator模式达成AOP
在使用Decorator模式时,其实是把同步逻辑作为PstnUser对象的一个额外职责,放在另外一个单独的Decorator对象中,从而消除上述味道。熟悉Decorator模式的读者肯定已经明白该如何做了,这里就不再赘述。更改后的代码如下:
// 定义一个Decorator模式所需要的接口
class IPstnUser
{
//. . .
public:
virtual void HookOn() = 0;
virtual void HookOff() = 0;
};
class PstnSynDecorator : public IPstnUser
{
public:
PstnSynDecorator(IPstnUser* user) : user_(user)
{ }
void HookOn()
{
user_->HookOn();
PstnSynAgent::AskForSynHookOn(user_->GetV5IID(),
user_->GetL3Addr());
}
void HookOff()
{
user_->HookOff();
PstnSynAgent::AskForSynHookOff(user_->GetV5IID(),
user_->GetL3Addr());
}
// . . .
private:
IPstnUser* user_;
};
class PstnUser : pubic IPstnUser
{
public:
void HookOn()
{
// do something corresponding to HookOn Event.
. . .
}
void HookOff()
{
// do something corresponding to HookOff Event.
. . .
}
// . . .
};
// 在创建对象时,需要这样进行
IPstnUser* pstnUser = new PstnUser(iid, addr) //创建普通PstnUser对象
IPstnUser* synPstnUser = new PstnSynDecorator(pstnUser) //为普通PstnUser对象增加同步功能。
这样,就达成了业务对象状态同步需求和PstnSynDecorator类直接对应,而Pstn用户端口的功能需求和PstnUser直接对应。
2) 使用Policy-Based Design达成AOP
在这种方法中,其实是把业务对象状态同步逻辑作为一个Policy,通过使用Policy-Based Design手法,把这个policy和业务对象逻辑本身组合起来,从而消除上述味道。Policy-Based Design技术在C++中是通过template和multiple-inheritance实现的。关于Policy-Based Design的详细介绍,请参见《Modern C++ Design》一书,这里也不再介绍。更改后的代码如下:
template <class SynPolicy, class UserPolicy>
class PstnUserWrapper : private SynPolicy, private UserPolicy
{
// . . .
public:
void HookOn()
{
UserPolicy::HookOn();
SynPolicy::AskForSynHookOn(GetV5IID(),GetL3Addr());
}
void HookOff()
{
UserPolicy::HookOff();
SynPolicy::AskForSynHookOff(GetV5IID(),GetL3Addr());
}
// . . .
};
class PstnUser
{
public:
void HookOn()
{
// do something corresponding to HookOn Event.
. . .
}
void HookOff()
{
// do something corresponding to HookOff Event.
. . .
}
// . . .
};
// 在创建对象时,需要这样进行
tyepdef PstnUserWrapper<PstnSynAgent, PstnUser > SynPstnUser;
SynPstnUser synPstnUser;
这种方法同样也解决了上面提到的问题,达成了需求元素和实现结构间的一一对应关系。相比起来,使用Decorator模式的方法具有更大的动态性,如果需要这种额外的动态性的话,可以选择使用Decorator模式的方法。
不过请注意,上面两种方法只是应用了AOP的思想,并不是完全意义上的AOP。在支持纯粹AOP的开发环境中,业务状态同步这种“横切”逻辑是由开发环境(编译器(扩展)或者run-time system等)根据相应的定义描述信息自动“织入”的,对象模型本身根本感觉不到这个“横切”逻辑的存在。在缺少AOP开发环境支持的情况下,我们只能通过手工的方法进行“织入”。
一点建议
如果在进行Use Case分析时能够正确运用AOP技术,我们就获取了另外一个纬度的自由。这个额外的自由度能为我们带来很多好处。首先,我们可以以更为干净、清晰的方式获取Use Case,使Use Case之间的耦合降到最低,并且还能最大程度地做到Use Case和实现结构间地一对一映射关系,这样开发团队就可以独立、并行地进行开发,大大提高开发生产力,并且有效地降低了开发小组间无谓的沟通、协调工作量。此外,这个自由度可以使我们在设计实现结构时更加模块化、更加富有表达力,从而为更加有效的迭代、重构提供了一个良好的基础。
但是,在我们进行开发时,一定要注意不要滥用AOP技术。和任何一种其他技术一样,AOP也是靠引入一定程度的代价来解决我们所面临的问题的。如果我们能够理解AOP所真正要解决的问题,在实际开发中正确的应用了这种技术,那么这种代价和所带来的好处相比是值得的。但是,如果不恰当地应用了AOP技术,那么非但不会带来任何好处,相反还会使得程序结构变得更加晦涩。希望上面的介绍能够为你在决定“是否该考虑使用AOP?”这个问题时提供一个依据。我相信如果你真得理解了上面对AOP的介绍,就不会在拿到了AOP这个“榔头”后,发现到处都是pointcut和advice之类的“钉子”了 :)
邓辉
软件开发领域一直一来的一个核心问题就是如何能够更好地满足Dijkstra所提出的separation of concerns原则。这个原则表达了代码和开发过程的一个最为重要的特性,但是遗憾的是它只是一个原则,其中并没有告诉我们如何做才能满足这一原则。人们在寻求能够满足这一原则的实现技术上进行了很多的探索,也取得了许多成就。其中,AOP(Aspect-Oriented Programming)就是这方面的一项最新技术。AOP的概念最初是由Xerox PARC研究中心的研究人员首先提出的,其目标是通过提供一些方法和技术,把问题领域分解成一系列的functional component和一系列横跨多个functional component的aspect,然后组合这些component和aspect,获得系统的实现。
在AOP提出很长一段时间内,基本上都处于一种学术研究和试验阶段。不过,近几年来,情况发生了改变。由于企业应用复杂度的不断提高,对软件开发技术提出了新的挑战。如何才能使得应用开发者仅仅关注于业务逻辑本身的开发,而不用纠缠于那些诸如安全、事务、日志等和业务逻辑无关但又是系统有效地执行业务逻辑所必须的功能呢?如果有某种技术能够把这些与具体业务逻辑无关但又是每个应用系统不可缺少的功能块,和业务逻辑做到干净的隔离,并且又能够通过一些简单的配置或者“胶水”代码,在需要时“织入”到应用系统中,那么不仅会大大提高开发效率,有效地降低软件的复杂性,而且在代码的清晰性、模块化、可测试性方面也会取得很好的提升。这正是AOP所要解决的问题,下面我将从系统分析、设计活动最为基本的目标入手,介绍一下AOP到底是什么,它解决了我们所面临的哪些困难。
AOP是什么?
在我们进行软件开发时,最为理想的情况应该是:每个需求概念元素都能够在源代码中有一个直接、清晰的表示。如果我们能够做到这一点,那么就能够在根本上把变化造成的影响局部化,而这正好是团队高效地的并行开发、架构顺畅地演化以及有效达成迭代的基础。
举例来说,在我们项目所开发的接入网软件系统中,PSTN用户端口这个概念就直接对应于程序中的一个设计表示:PstnUser类。V5链路这个概念也直接对应于一个设计表示:Link类。下面我们来考虑这样一个需求:由于电信级的设备都有较高的可靠性要求,所以系统必须要提供主备的功能。为了能够实现主备功能,除了硬件方面的一些要求外,在软件方面我们还必须要做到当一个业务对象的状态发生变化时,我们必须要把该状态及时地同步到备机以保证在主备切换后,系统还能够提供正常的业务功能。可以看出这是一个单一的需求描述,但是就是这样一个简单的单一功能需求却很难被映射到一个单一的设计表示结构上面。一般来说,我们会在业务对象中状态需要同步的地方都放上触发同步的逻辑。这种实现方法背离了需求和实现间一一映射的目标,相反这是一种需求到实现间的一对多的映射关系。
那些在设计中所捕捉到的需求元素,一般来说很可能会随着软件开发的进展、程序的演化而发生变化。如果在我们的程序中做到了需求元素和实现结构间的一一映射,那么这个实现结构就会非常易于修改和维护。相反,如果是一个一到多映射,那么在进行修改或者维护时就会非常得困难,因为在实现结构中有多个地方都得被更新,并且还得保证这些更新是完全且一致的。如果遗漏掉一个地方,或者更新时没有做到一致,那么就会造成一些非常微妙的bug,定位出这些bug很可能会花费你不少时间。
现在,我们换一个角度来看看这个问题。我们希望每个需求概念元素都能清晰地映射到一个实现结构上。我们同样也希望每个实现结构能够清晰地映射到唯一的一个需求概念元素上。但是,在上面所列举的系统主备的例子中,我们看到了业务对象状态同步这个需求概念元素散布在实现当中,这也意味着在业务对象实现中,不得不处理多种需求元素。对于PstnUser来说,它现在得处理至少两种需求概念:1、实现PSTN用户端口本身的逻辑;2、在需要时进行业务状态的同步。这是一种典型的需求概念元素和实现结构间的多对一的映射关系,敏锐的读者一定会发现这种情况违反了SRP(Single Responsibility Principle)。
这种多对一的情况同样也会带来很多问题。因为在一个类中,除了要实现本身要表示的那个需求概念外,还不得不处理很多和这个需求概念毫无关系的其他要素,比如:同步、事务、安全以及持久化等。这些和该类毫无关系的元素不但使得该类具有多个变化的原因,同时也对该类造成了很大的侵入性,使得该类极难重用、测试。
一般来说,我们在真正的软件开发中所遇到的真实情况更加复杂,我们基本上不可能遇到理想的概念和实现间的一一映射,而是多对多的映射。这也是导致软件如此难以维护、难以理解、如此复杂的原因。虽然面向对象方法在解决这个问题上取得了不小的进步,但是在日益复杂的问题领域面前,它还无法使我们完全做到需求概念元素和实现结构之间清晰、干净的映射关系。而这正是Aspect-Oriented Programming试图要解决的问题。通过AOP,我们在实现一一映射这个目标上又迈进了一大步。在AOP中引入了一个新的构造元素aspect,通过它我们可以达成在OOP中无法做到的需求概念元素和实现结构间的一一映射,比如:前面提到的系统业务状态同步需求。
请记住,我们所追求的就是需求概念元素和实现结构之间的一一映射。无论何时,无论何种原因,只要背离了这个目标,我们都会遇到麻烦。Structural Programming、Object-Based Programming、Object-Oriented Programming、Functional programming、Generic Programming以及Aspect-Oriented Programming这些编程范型都是为了在某个纬度上帮助我们更近地达成这个目标。
案例研究
一般来说,完全的AOP开发是需要开发环境支持的,比如:AspectJ、Spring或者JBOSS。对于C++语言来说,到目前为止还没有一个经过实践检验的支持AOP的开发环境出现,更不用说嵌入式系统了。不过,我们还是可以通过一些惯用的技术来手工地达成AOP的效果。在本小节中,我会通过项目中的一个实例来介绍一下比较常用的两种技术。我还以上面提到过的PstnUser对象状态同步为例进行介绍。传统实现方法的代码片断如下:
class PstnSynAgent
{
// . . .
public:
static void AskForSynHookOn(V5IID, L3ADDR);
static void AskForSynHookOff(V5IID, L3ADDR)
// . . .
};
void PstnUser::HookOn()
{
// do something corresponding to HookOn Event.
. . .
PstnSynAgent::AskForSynHookOn(GetV5IID(), GetL3Addr() );
}
void PstnUser::HookOff()
{
// do something corresponding to HookOff Event.
. . .
PstnSynAgent::AskForSynHookOff(GetV5IID(), GetL3Addr() );
}
可以看出,上面的代码片断中存在我们在前面所提到的问题,同步逻辑散布在PstnUser对象中那些需要同步状态的方法中。而PstnUser对象本身也违反了SRP,它不但要完成PstnUser本身的逻辑,还得去完成同步触发逻辑。下面我们将通过两种惯用的技术手法来达成AOP以消除上述味道,这两种方法是:Decorator模式以及Policy-Based Design。
1) 使用Decorator模式达成AOP
在使用Decorator模式时,其实是把同步逻辑作为PstnUser对象的一个额外职责,放在另外一个单独的Decorator对象中,从而消除上述味道。熟悉Decorator模式的读者肯定已经明白该如何做了,这里就不再赘述。更改后的代码如下:
// 定义一个Decorator模式所需要的接口
class IPstnUser
{
//. . .
public:
virtual void HookOn() = 0;
virtual void HookOff() = 0;
};
class PstnSynDecorator : public IPstnUser
{
public:
PstnSynDecorator(IPstnUser* user) : user_(user)
{ }
void HookOn()
{
user_->HookOn();
PstnSynAgent::AskForSynHookOn(user_->GetV5IID(),
user_->GetL3Addr());
}
void HookOff()
{
user_->HookOff();
PstnSynAgent::AskForSynHookOff(user_->GetV5IID(),
user_->GetL3Addr());
}
// . . .
private:
IPstnUser* user_;
};
class PstnUser : pubic IPstnUser
{
public:
void HookOn()
{
// do something corresponding to HookOn Event.
. . .
}
void HookOff()
{
// do something corresponding to HookOff Event.
. . .
}
// . . .
};
// 在创建对象时,需要这样进行
IPstnUser* pstnUser = new PstnUser(iid, addr) //创建普通PstnUser对象
IPstnUser* synPstnUser = new PstnSynDecorator(pstnUser) //为普通PstnUser对象增加同步功能。
这样,就达成了业务对象状态同步需求和PstnSynDecorator类直接对应,而Pstn用户端口的功能需求和PstnUser直接对应。
2) 使用Policy-Based Design达成AOP
在这种方法中,其实是把业务对象状态同步逻辑作为一个Policy,通过使用Policy-Based Design手法,把这个policy和业务对象逻辑本身组合起来,从而消除上述味道。Policy-Based Design技术在C++中是通过template和multiple-inheritance实现的。关于Policy-Based Design的详细介绍,请参见《Modern C++ Design》一书,这里也不再介绍。更改后的代码如下:
template <class SynPolicy, class UserPolicy>
class PstnUserWrapper : private SynPolicy, private UserPolicy
{
// . . .
public:
void HookOn()
{
UserPolicy::HookOn();
SynPolicy::AskForSynHookOn(GetV5IID(),GetL3Addr());
}
void HookOff()
{
UserPolicy::HookOff();
SynPolicy::AskForSynHookOff(GetV5IID(),GetL3Addr());
}
// . . .
};
class PstnUser
{
public:
void HookOn()
{
// do something corresponding to HookOn Event.
. . .
}
void HookOff()
{
// do something corresponding to HookOff Event.
. . .
}
// . . .
};
// 在创建对象时,需要这样进行
tyepdef PstnUserWrapper<PstnSynAgent, PstnUser > SynPstnUser;
SynPstnUser synPstnUser;
这种方法同样也解决了上面提到的问题,达成了需求元素和实现结构间的一一对应关系。相比起来,使用Decorator模式的方法具有更大的动态性,如果需要这种额外的动态性的话,可以选择使用Decorator模式的方法。
不过请注意,上面两种方法只是应用了AOP的思想,并不是完全意义上的AOP。在支持纯粹AOP的开发环境中,业务状态同步这种“横切”逻辑是由开发环境(编译器(扩展)或者run-time system等)根据相应的定义描述信息自动“织入”的,对象模型本身根本感觉不到这个“横切”逻辑的存在。在缺少AOP开发环境支持的情况下,我们只能通过手工的方法进行“织入”。
一点建议
如果在进行Use Case分析时能够正确运用AOP技术,我们就获取了另外一个纬度的自由。这个额外的自由度能为我们带来很多好处。首先,我们可以以更为干净、清晰的方式获取Use Case,使Use Case之间的耦合降到最低,并且还能最大程度地做到Use Case和实现结构间地一对一映射关系,这样开发团队就可以独立、并行地进行开发,大大提高开发生产力,并且有效地降低了开发小组间无谓的沟通、协调工作量。此外,这个自由度可以使我们在设计实现结构时更加模块化、更加富有表达力,从而为更加有效的迭代、重构提供了一个良好的基础。
但是,在我们进行开发时,一定要注意不要滥用AOP技术。和任何一种其他技术一样,AOP也是靠引入一定程度的代价来解决我们所面临的问题的。如果我们能够理解AOP所真正要解决的问题,在实际开发中正确的应用了这种技术,那么这种代价和所带来的好处相比是值得的。但是,如果不恰当地应用了AOP技术,那么非但不会带来任何好处,相反还会使得程序结构变得更加晦涩。希望上面的介绍能够为你在决定“是否该考虑使用AOP?”这个问题时提供一个依据。我相信如果你真得理解了上面对AOP的介绍,就不会在拿到了AOP这个“榔头”后,发现到处都是pointcut和advice之类的“钉子”了 :)