面向切面编程(AOP)是一种编程范式,它允许程序员将横切关注点(如日志记录、事务管理、安全检查等)与业务逻辑分离开来。这种分离可以提高代码的模块化和可维护性。尽管.NET平台没有内置相关的工具,但有一些第三方解决方案提供了这种能力,例如PostSharp、Unity、Spring.NET、Castle Windsor和Aspect.NET等。选择一个AOP框架时,需要考虑实现横切功能注入的机制及其优缺点。通常,可以识别出两种方法:编译时注入和运行时代理类生成。
编译时注入是首选选项,因为它在程序执行期间不需要额外的计算开销,这对于移动设备尤为重要。而常规的代理类生成虽然实现起来更简单,但除了计算开销外,还有一些限制——方法和属性必须属于接口或为虚拟的,才能通过代理类进行拦截。
PostSharp为面向切面编程提供了强大的功能,但它是一个商业产品,可能不适合某些项目。作为替代方案,开发并正在不断改进AspectInjector——一个允许在编译时应用切面的框架,它具有简单但灵活的接口。
让快速看看AspectInjector能做什么。可以使用以下命令将库添加到项目中:
Install-Package AspectInjector -Pre
目前只有预发布版本具有完整的功能集,所以需要"-Pre"开关来获取Aspect Injector的最完整功能。
假设需要为所有方法添加一个简单的跟踪——只需捕获方法的开始和结束。为了实现这一点,需要定义一个实现所需行为的切面:
public class TraceAspect
{
[Advice(InjectionPoints.Before, InjectionTargets.Method)]
public void TraceStart([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name)
{
Console.WriteLine("[{0}] Method {1}.{2} started", DateTime.UtcNow, type.Name, name);
}
[Advice(InjectionPoints.After, InjectionTargets.Method)]
public void TraceFinish([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name)
{
Console.WriteLine("[{0}] Method {1}.{2} finished", DateTime.UtcNow, type.Name, name);
}
}
这意味着TraceStart和TraceFinish方法将在每个方法的开始和结束时被调用,每次调用都将具有目标方法的名称及其声明类型的信息。
要将切面应用于需要它的任何类,只需用Aspect属性标记目标类:
[Aspect(typeof(TraceAspect))]
public class SampleClass
{
public void Method1()
{
// ...
}
public int Method2()
{
// ...
}
}
结果,TraceStart和TraceFinish的调用将被注入到SampleClass的所有方法中。以下是反编译后的代码:
public class SampleClass
{
// Added by AspectInjector
private TraceAspect __a$i_TraceAspect;
public void Method1()
{
// Added by AspectInjector
this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method1");
Console.WriteLine("Inside Method1");
// Added by AspectInjector
this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method1");
}
public int Method2()
{
// Added by AspectInjector
this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method2");
Console.WriteLine("Inside Method2");
// Added by AspectInjector
int num = 1;
int result = num;
this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method2");
return result;
}
public SampleClass()
{
// Added by AspectInjector
this.__a$_initializeInstanceAspects();
}
// Added by AspectInjector
private void __a$_initializeInstanceAspects()
{
if (this.__a$i_TraceAspect == null)
{
this.__a$i_TraceAspect = new TraceAspect();
}
}
}
这只是最简单示例之一,并没有展示AspectInjector的所有功能。有关注入目标、目标成员过滤(由Aspect属性上的附加参数控制)和建议参数源的更多选项,可以在文档中找到所有这些信息。
有一些特性值得特别关注——例如"Around"注入点。它目前只在Aspect Injector的预发布版本中可用,允许完全包装任何调用。这里最简单的示例之一是跟踪调用持续时间:
public class DurationAspect
{
[Advice(InjectionPoints.Around, InjectionTargets.Method)]
public object TraceDuration([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name,
[AdviceArgument(AdviceArgumentSource.Target)] Func
主要思想是,带有InjectionPoints.Around的建议必须接受函数委托和相应的参数——进行原始调用所需的所有必要信息。建议方法可以在调用原始方法之前和之后执行任何额外的工作,如示例所示。然而,"around"注入比"before"和"after"在代码更改方面更重。虽然后两者修改原始方法的主体,"around"创建额外的包装函数,因此建议仅在需要在方法开始和结束之间传递某些数据时使用它。
可能需要注入的不仅仅是一些方法、属性或事件的额外代码,还有额外的接口。一个经典的例子是.NETUI框架的INotifyPropertyChanged:
[AdviceInterfaceProxy(typeof(INotifyPropertyChanged))]
public class NotifyPropertyChangedAspect : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
[Advice(InjectionPoints.After, InjectionTargets.Setter)]
public void RaisePropertyChanged(
[AdviceArgument(AdviceArgumentSource.Instance)] object targetInstance,
[AdviceArgument(AdviceArgumentSource.Name)] string propertyName)
{
PropertyChanged(targetInstance, new PropertyChangedEventArgs(propertyName));
}
}
之后,将不得不用[Aspect(typeof(NotifyPropertyChangedAspect))]属性装饰所有视图模型类,它们的所有属性将自动变得可通知。