面向方面编程用于基准测试






4.64/5 (15投票s)
本文解释了如何将 AOP 用于基准测试目的。
引言
在本文中,我将尝试演示如何逐步使用面向方面编程来开发一个非侵入式、简单的基准测试应用程序。
本文附带了一个 C# 示例解决方案(包含源代码),其中包含以下项目:
- Benchmarking:一个类库项目,提供通用基准测试功能。
- MyApplication.CommandCore:一个包含实现命令模式的计算器库。因此,它可以执行撤消和重做操作。
- MyApplication.CommandClient:一个控制台应用程序,它引用 MyApplication.CommandCore 项目并检索计算器操作的性能信息。
- Sorting:一个类库项目,包含各种排序器对象的实现。
- MyApplication.Statistics:一个类库项目,包含一个执行统计计算的类(
Statistics
)。 - MyApplication.SortingStrategy:一个 Windows Forms 项目,它引用
MyApplication.Statistics
并检索和显示各种现有排序算法的Sort
方法的性能信息。
当然,有许多可用的框架提供性能功能。因此,本文的目的不是通过提供新的基准测试工具来重复造轮子。相反,重点是探索 AOP 可能非常有用的领域。
背景
定义 AOP
由于我使用了 Spring.NET 框架来开发示例代码,因此我将使用 Spring.NET 的定义来描述面向方面编程:
“面向方面编程 (AOP) 通过提供另一种思考程序结构的方式来补充 OOP。OOP 将应用程序分解为对象层次结构,而 AOP 将程序分解为方面或关注点。这使得事务管理等关注点的模块化成为可能,否则这些关注点会跨越多个对象(此类关注点通常称为横切关注点)。”
上述定义提到了事务管理作为程序之间共同关注点(或方面)的一个例子,但我们还可以列举其他跨越多个对象的关注点:
- 日志记录,
- 安全,
- 模拟框架(例如在 TypeMock 中),
- 异常处理,当然还有……
- 基准测量
AOP 术语
为了参考和澄清,我将列举本文中使用的一些 AOP 概念(也根据 Spring.NET 文档):
- 方面(Aspect):一个关注点的模块化,其实现可能跨越多个对象。事务管理是企业应用程序中横切关注点的一个很好的例子。方面在 Spring.NET 中实现为通知器或拦截器。
- 通知(Advice):AOP 框架在特定连接点采取的行动。不同类型的通知包括“环绕”、“前置”和“抛出”通知。通知类型将在下面讨论。包括 Spring.NET 在内的许多 AOP 框架将通知建模为拦截器,在连接点“周围”维护一个拦截器链。
- 切入点(Pointcut):一组连接点,指定何时触发通知。AOP 框架必须允许开发人员指定切入点:例如,使用正则表达式。
- 目标对象(Target object):包含连接点的对象。也称为被通知对象或代理对象。
- 前置通知(Before advice):在连接点之前执行的通知,但没有能力阻止执行流继续到连接点(除非它抛出异常)。
- 返回后通知(After returning advice):在连接点正常完成之后执行的通知:例如,如果方法返回而没有抛出异常。
可撤销/可重做计算器
MyApplication.CommandCore 提供了一个可撤销的 4 运算计算器类,供 MyApplication.CommandClient 项目使用。您可以执行操作,然后调用 Undo(n)
方法。因此,计算器将回滚最新的 n 个操作。
计算器项目中的顶级对象是 User
类。客户端应用程序执行计算器操作的方式如下:
class Program
{
static void Main(string[] args)
{
// Create user and let her compute
User user = new User();
user.Initialize();
user.Compute('+', 100);
user.Compute('-', 50);
user.Compute('*', 10);
user.Compute('/', 2);
// Undo 4 commands
user.Undo(4);
// Redo 3 commands
user.Redo(3);
// Wait for user
Console.In.Read();
}
}
上述代码(直到求和操作行)在序列图中看起来像这样:
好的,现在假设您有兴趣测量计算器的性能。对于这个简单的任务,您可以从在 User
类的开头和结尾添加代码行开始,这样您以后可以比较时间并计算执行时间:
public void Compute(char @operator, int operand)
{
RegisterThisEntryTimeSomewhere();
// Create command operation and execute it
Command command = new CalculatorCommand(
calculator, @operator, operand);
command.Execute();
// Add command to undo list
commands.Add(command);
current++;
RegisterThisReturnTimeSomewhere();
}
但是等等,我们还没有完成。User
类还有其他方法,比如“Undo
”:
public void Undo(int levels)
{
RegisterThisEntryTimeSomewhere();
Console.WriteLine("\n---- Undo {0} levels ", levels);
// Perform undo operations
for (int i = 0; i < levels; i++)
{
if (current > 0)
{
Command command = commands[--current] as Command;
command.UnExecute();
}
}
RegisterThisReturnTimeSomewhere();
}
嗯,看来我们有麻烦了。我们要改变每一个我们试图测量的方法吗?显然,改变我们酷对象的所有方法不是一个好主意。此外,有些情况下我们甚至无法改变我们试图测量的对象的代码。这就是面向方面编程可以发挥巨大价值的地方。
基准测试项目
让我们创建一个名为 Benchmarking 的新项目,以便提供 AOP 功能,使我们的客户端应用程序能够进行通用性能测量。让我们写下我们新的 Benchmarking 项目的指导原则:
- 它应该使用 Spring.Net AOP 功能。
- 它应该为用户提供一个简单的界面和非侵入式的 AOP 功能。
- 它应该暴露创建新的代理对象的功能。创建代理对象后,它应该根据配置文件开始注册一个包含我们想要测量对象成员执行时间的时间表。
- 时间表的每一行将包含以下信息:类名、成员名(即方法/属性名)、参数列表和耗时。
- 我们应该能够在任何时候检索时间表。
- 我们应该能够重置时间表,以便可以重新开始性能测量。
BenchmarkFactory
首先为 IoC 容器(=控制反转容器,一种实现控制反转模式的技术。有关 IoC 模式的更多信息,请参阅 Billy McCafferty 的优秀文章 用于松耦合的依赖注入)实例化一个静态 ApplicationContext
。控制反转将为我们提供仅通过更改客户端应用程序的 XML 配置文件即可实例化不同类的灵活性。
// Create AOP proxy using Spring.NET IoC container.
private static IApplicationContext ctx = ContextRegistry.GetContext();
private static List<timerow> timeSheet = new List<timerow>();
BenchmarkFactory
类的核心是 GetProxy()
方法。它只有一行,并返回通过 Spring.NET 框架提供的 IoC 容器技术创建的对象:
public static object GetProxy(string objectName)
{
return ctx[objectName];
}
调用 GetProxy()
方法时,AOP 框架会为请求的类生成一个动态代理,其中包含额外的代码,使对象的成员能够在调用生命周期中被拦截。拦截器是 Benchmarking 项目的 BenchmarkAroundAdvice
类。
回到客户端计算器项目
现在我们有了基准测试层,让我们开始修改我们的客户端计算器项目。首先,客户端计算器项目将有一个新的 app.config 文件,其中包含 AOP 框架的配置。此配置信息包括:
- 关于代理对象的信息:代理类的全名(命名空间 + 类名)、程序集名称和拦截器名称。
- 环绕通知信息:通知器类的全名、程序集名称,以及一个带有映射名称的切入点,该切入点定义了将要测量的类成员。
<object id="aroundAdvisor"
type="Spring.Aop.Support.NameMatchMethodPointcutAdvisor, Spring.Aop">
<property name="Advice">
<object type="Benchmarking.Aspects.BenchmarkAroundAdvice, Benchmarking" />
</property>
<property name="MappedNames">
<list>
<value>*</value>
</list>
</property>
</object>
这就是我们如何避免修改每个想要测量类的繁琐任务的方法……
现在,我们应该替换用户对象的实例化方法。我们将使用 BenchmarkFactory.GetProxy()
方法,而不是使用 new
关键字:
IUser user = (IUser)BenchmarkFactory.GetProxy("user");
请注意,需要上述行,以便 AOP 框架可以创建一个动态代理,拦截每个方法/属性调用的开始和结束。
在 GetProxy()
行之后,我们必须立即重置 BenchmarkFactory
的时间表:
BenchmarkFactory.ResetTimeSheet();
完成对计算器对象的操作后,我们可以遍历时间表行,以便列出每个方法调用的性能:
Console.WriteLine("===============================");
Console.WriteLine("BENCHMARKING RESULTS");
Console.WriteLine("===============================");
foreach (TimeRow tr in timeSheet)
{
Console.WriteLine("===============================");
Console.WriteLine("Class Name = {0}", tr.ClassName);
Console.WriteLine("Member Name = {0}", tr.MemberName);
Console.WriteLine("Arguments = {0}", tr.ArgList);
Console.WriteLine("Elapsed time = {0} milliseconds", tr.ElapsedTime);
}
Console.WriteLine("===============================");
更改后,这是应用程序的新序列图。请注意,代理的 IUser
对象现在在每次方法调用周围被拦截。一旦切入点规范匹配,相应的环绕通知类就会自动触发:
现在,我们可以再次运行应用程序,第一次查看我们的性能结果:
可视化基准测试
让我们打开 Benchmarking 项目,创建一个表示时间表的 Windows 窗体:
这个新的用户界面由一个单例窗体提供,当您有另一个 Windows Forms 项目引用了 Benchmarking 项目,并且您希望在后台处理基准测试结果,但仅当用户明确要求应用程序这样做时(例如,通过单击“帮助”菜单下的“基准测试”子菜单)才显示时,此界面非常有用。
此界面还允许用户将时间表数据复制到剪贴板。因此,复制的数据可以直接粘贴到 Excel 工作表中。此外,用户可以随时重置时间表数据。
Windows Forms 计算器项目
现在我们可以在 Windows 窗体中显示基准测试数据,让我们创建一个新项目 (CommandUI),并为我们的旧控制台计算器实现一个新的 Windows Forms 界面:
新的界面很酷。现在我们可以执行 4 种运算,加上撤销/重做功能。但这里最大的飞跃是能够显示基准测试结果。经过一些计算,当点击“显示基准测试”按钮时,我们得到以下结果:
请注意,显示了 User
类的两个成员:Computed
和 get_CurrentValue
。Spring.NET AOP 框架允许我们修改 App.config,以便我们可以指定要显示的成员。因此,我们打开 App.config,并将 aroundAdvisor
配置中的“MappedNames
”属性内的“*”值替换为“Compute”。
<object id="aroundAdvisor"
type="Spring.Aop.Support.NameMatchMethodPointcutAdvisor, Spring.Aop">
<property name="Advice">
<object type="Benchmarking.Aspects.BenchmarkAroundAdvice, Benchmarking" />
</property>
<property name="MappedNames">
<list>
<value>Compute</value>
</list>
</property>
</object>
修改后的时间表如下所示:
不幸的是,我们测试的方法速度太快,基准测试无法感知。如果我们创建一个更具挑战性的示例怎么办?
排序项目和排序策略项目
假设你被分配了一个任务,创建一个应用程序来执行密集的统计计算。你发现你将在应用程序的许多地方使用排序算法。(好吧,我们知道 .NET Framework 有很多具有排序功能的对象,但让我们暂时忘记它……;-))你还发现排序算法的效率取决于要排序的元素数量。因此,你创建了两个项目:排序项目,包含不同的排序算法,和排序策略项目,用于测试这些算法的效率。
这些是要测试的排序算法类:
BiDirectionalBubbleSort(双向冒泡排序)
BubbleSorter(冒泡排序器)
ComboSort11(组合排序11)
ComparableComparer(可比较比较器)
DoubleStorageMergeSort(双存储归并排序)
FastQuickSorter(快速快速排序器)
HeapSort(堆排序)
InPlaceMergeSort(原地归并排序)
InsertionSort(插入排序)
OddEvenTransportSorter(奇偶传输排序器)
QuickSorter(快速排序器)
QuickSortWithBubbleSort(带冒泡排序的快速排序)
SelectionSort(选择排序)
ShakerSort(摇曳排序)
ShearSorter(剪切排序器)
ShellSort(希尔排序)
由于每个排序类都实现了一个具有公共 Sort
方法的 ISort
接口,我们必须让 app.config 指定我们的环绕通知器只拦截这些类的 Sort
方法:
<object id="aroundAdvisor"
type="Spring.Aop.Support.NameMatchMethodPointcutAdvisor, Spring.Aop">
<property name="Advice">
<object type="Benchmarking.Aspects.BenchmarkAroundAdvice, Benchmarking" />
</property>
<property name="MappedNames">
<list>
<value>Sort</value>
</list>
</property>
</object>
排序策略项目只有一个表单,旨在测量不同元素数量(标记)下排序算法的执行时间:
您还可以点击“复制到剪贴板”按钮,然后将数据直接粘贴到 Excel 工作表中。之后,您可以创建一个图表,更清晰地查看每个排序算法在不同元素数量下的效率演变:
结论
我希望这个简单的例子能让读者对 Spring.NET 框架提供的面向方面编程的实际用法有所了解。如果您读到了本文的这一部分,非常感谢您的耐心。任何关于本文或示例的评论、建议或投诉都将不胜感激。
历史
- 2007/06/13 - 初次发布。
- 2007/07/01 - 代码和文章已更新(前置和后置通知已替换为环绕通知)。