通过拦截在 Windows Phone 上实现横切关注点





5.00/5 (3投票s)
本文介绍了如何将面向切面编程实践引入 WP7 平台
- 引言
- 示例
- 有什么问题?
- 拦截
- 依赖注入容器
- Windows Phone 实现
- 代理类
- 代理映射
- 代理自动生成
- 结果
- 如何使用:SecureBox 项目
- 设置 PhoneCore 框架
- 摘要
- 链接
- 推荐书籍
- 历史
引言
首先,我想通过一个例子来介绍本文的主要思想,我认为这是最好的方式。
示例
例如,你需要实现一个简单的计算器,它只支持两个简单的运算:加法和乘法。这是一个简单的实现:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Mul(int a, int b)
{
return a*b;
}
}
它工作得很好,并且代码的意图很清晰:只是对两个数字进行运算。然而,如果你这样调用 Add/Mul 方法:
calc.Add(Int32.MaxValue, 100);
calc.Mul(Int32.MaxValue, 100) ;
你将会得到一个负整数。不幸的是,这个结果对于服务消费者来说是不可接受的。这可以通过 `checked` 关键字来修复:
public class Calculator
{
public int Add(int a, int b)
{
checked
{
return a + b;
}
}
public int Mul(int a, int b)
{
checked
{
return a*b;
}
}
}
太棒了!但是有一天,生产环境中出现了与服务消费者相关的 bug,你想跟踪所有的方法调用。
public class Calculator
{
public int Add(int a, int b)
{
Log.Write("Calculator.Add: a={0}, b={1}", a, b);
checked
{
return a + b;
}
}
public int Mul(int a, int b)
{
Log.Write("Calculator.Mul: a={0}, b={1}", a, b);
checked
{
return a*b;
}
}
}
好了!之后,你的服务消费者抱怨说你的计算器在生产环境中运行得太慢了。你解释说数字的操作非常快,但并没有奏效。更糟糕的是,你无法在生产环境中附加性能分析器,所以你决定测量方法的执行时间:
public class Calculator
{
Stopwatch watch = new Stopwatch();
public int Add(int a, int b)
{
Log.Write("Calculator.Add: a={0}, b={1}", a, b);
watch.Restart();
checked
{
var c = a + b;
}
watch.Stop();
Log.Write("Calculator.Add: {0} elapsed ms", watch.ElapsedMilliseconds);
return c;
}
public int Mul(int a, int b)
{
Log.Write("Calculator.Mul: a={0}, b={1}", a, b);
watch.Restart();
checked
{
var c = a * b;
}
watch.Stop();
Log.Write("Calculator.Mul: {0} elapsed ms", watch.ElapsedMilliseconds);
return c;
}
}
此时,你注意到与第一个版本相比,有什么地方不对劲,于是你决定进行重构。
public class Calculator
{
Stopwatch watch = new Stopwatch();
public int Add(int a, int b)
{
return Execute("Add", () => { checked { return a + b; } });
}
public int Mul(int a, int b)
{
return Execute("Mul", () => { checked { return a * b; } });
}
private int Execute(string methodName, Func<int> expression)
{
Log.Write("{0}: a={1}, b={2}", methodName, a, b);
watch.Restart();
int c;
c = expression.Invoke();
watch.Stop();
Log.Write("{0}: {1} elapsed ms",methodName, watch.ElapsedMilliseconds);
return c;
}
}
有什么问题?
这个例子可以通过新的需求进行扩展,但我认为现在是时候看看即时结果了。代码工作得很完美,并提供了以下功能:
- 日志记录
- 错误处理
- 分析
但是,有一天你需要实现一个新的服务,该服务支持所有这些功能以及新的功能。该怎么办?使用 Helper 类来提取这个功能?这似乎是答案,但事实并非如此。你的类已经依赖于日志子系统、性能分析方法、额外的内部行为等。这有以下几个原因是不正确的:
- 代码重用 - 依赖关系使得代码重用过程更加困难。
- 违反 单一职责原则 - 一个类应该只有一个职责,它应该只做一件事并且做好。
- 代码的实际作用不清楚 - 这与前一个原因有关。
实际上,日志记录、性能分析等是与“横切关注点”相关的概念。
“在计算机科学中,横切关注点是指影响其他关注点的程序的各个方面。这些关注点通常无法在设计和实现上与系统的其余部分清晰地分解,并可能导致代码散布(代码重复)、纠缠(系统之间存在显著依赖关系)或两者兼有”[1]
这个术语对于理解 面向切面编程 [2] 至关重要。对于上面的例子,即使我们提取了日志记录和性能分析的方面,我们仍然需要使用它们而没有显式的依赖。这可以通过 AOP 技术实现——拦截。
拦截
拦截的概念很简单:我们希望能够拦截消费者和服务之间的调用,并在服务调用之前或之后执行一些代码。
拦截的典型实现是 GoF 的 装饰器 模式。它描述了如何“动态地为对象附加附加的职责。装饰器提供了一种灵活的替代方法来通过子类化来扩展功能”(1)。尽管如此,这种方法要求你向服务消费者返回装饰器类的实例,而不是真实的对象。拥有一个工具来帮助你组合对象图、管理对象之间的依赖关系并在必要时对其进行装饰是一个很好的起点。这可以通过 依赖注入 容器来实现。
依赖注入容器
依赖注入容器 是一个很大的话题,我将不做深入探讨。我只想推荐 Mark Seemann 的书 “Dependency Injection in .NET” (2)。简而言之,DI 容器提供了以下功能:
- 管理对象组合
- 拦截
- 生命周期管理
有很多知名的 DI 容器:
- Unity
- Castle Windsor
- Spring.NET
- StructureMap
但是,如果你正在为 Windows Phone 平台 编程,你的列表会更短。因此,下面的示例使用了 PhoneCore Framework 0.6.3 的 DI 容器的自定义实现,并展示了如何使用编译时生成的代理类来拦截方法调用。
Windows Phone 实现
这一部分描述了本文的主要思想:使用 PhoneCore Framework 在 Windows Phone 上拦截方法调用。首先,没有魔术,因为 WP7 不允许你动态加载不在部署包(xap 文件)中的程序集。因此,使用 Reflection API 或 Mono.Cecil 创建动态代理类是没有意义的,因为你无法在 AppDomain 中加载它们。在 AOP 方面实现拦截只有一种方法:编译时织入。这意味着动态代理应该在编译时生成,并作为部署 xap 文件的一部分进行部署。当客户端调用 DI 容器的 Resolve 方法(在 PhoneCore Framework 当前实现中通过属性注入使用 DependencyAttribute 显式或隐式调用)时,将返回代理类而不是原始对象。它包含每个接口方法中的特殊逻辑,这些逻辑会运行自定义行为(方面)。
PhoneCore Framework 的拦截引擎当前实现存在一些限制:
- 具体类型应在容器中注册,例如:Container.RegisterType<IClass,Class>()
- 类型应该是非泛型(但支持泛型方法)。
- 具体类型应通过容器显式或隐式使用,例如:Container.Resolve<IClass>()
- 特殊的 Decorator(代理)类应在编译时生成,映射到接口类型并部署到 xap 包中。
代理类
SecureBox 项目是这个概念的一个很好的例子。让我们看看 SettingsService 类的魔术代理示例:
using PhoneCore.Framework.IoC.Interception.Proxies;
namespace PhoneCore.Framework.Storage
{
public class SettingServiceProxy : ProxyBase, PhoneCore.Framework.Storage.ISettingService
{
public void Save(System.String key, System.Object value)
{
var methodInvocation = BuildMethodInvocation(MethodBase.GetCurrentMethod(), key, value);
RunBehaviors(methodInvocation);
}
public System.Boolean IsExist(System.String key)
{
var methodInvocation = BuildMethodInvocation(MethodBase.GetCurrentMethod(), key);
return RunBehaviors(methodInvocation).GetReturnValue<System.Boolean>();
}
public T Load<T>(System.String key)
{
var methodInvocation = BuildMethodInvocation(MethodBase.GetCurrentMethod(), key);
methodInvocation.GenericTypes.Add(typeof(T));
return RunBehaviors(methodInvocation).GetReturnValue<T>();
}
}
}
正如你所见,它继承自 ProxyBase 类并实现了 ISettingsService。它已经作为 cs 文件包含在解决方案中。ProxyBase 类公开了一些辅助方法,这些方法构建方法调用上下文并运行附加行为(方面)链,例如日志记录、性能分析、验证。调用上下文存储实际参数和方法签名,行为可以对此进行使用。
namespace PhoneCore.Framework.IoC.Interception.Behaviors
{
/// <summary>
/// Executes and measures execution time
/// </summary>
public class ProfileBehavior: ExecuteBehavior
{
...
public override IMethodReturn Invoke(MethodInvocation methodInvocation)
{
Stopwatch watch = new Stopwatch();
watch.Start();
var result = base.Invoke(methodInvocation);
watch.Stop();
__trace.Info(__category, string.Format("{0}.{1} execution time: {2} ms",
methodInvocation.Target.GetType(), methodInvocation.MethodBase.Name, watch.ElapsedMilliseconds));
return result;
}
}
}
所有行为都实现了简单的接口:
using System;
using PhoneCore.Framework.Configuration;
namespace PhoneCore.Framework.IoC.Interception.Behaviors
{
/// <summary>
/// Represents an additional behavior of method invocation
/// </summary>
public interface IBehavior: IConfigurable
{
/// <summary>
/// The name of behavior
/// </summary>
string Name { get; set; }
/// <summary>
/// Provides the way to attach additional behavior to method
/// </summary>
/// <param name="methodInvocation"></param>
/// <returns></returns>
IMethodReturn Invoke(MethodInvocation methodInvocation);
}
}
所以,这是实现日志记录、性能分析等的地方。
代理映射
你可以通过两种方式将代理映射到接口:
• 配置
<interception> <!-- default behaviors --> <behaviors /> <components> <component interface="PhoneCore.Framework.UnitTests.Stubs.Container.IClassA,PhoneCore.Framework.UnitTests" proxy="PhoneCore.Framework.UnitTests.Stubs.Container.ClassAProxy,PhoneCore.Framework.UnitTests" name ="ClassAProxy"> <behaviors> <clear /> <behavior name="execute" type="PhoneCore.Framework.IoC.Interception.Behaviors.ExecuteBehavior,PhoneCore.Framework" /> <behavior name="trace" type="PhoneCore.Framework.IoC.Interception.Behaviors.TraceBehavior,PhoneCore.Framework" /> </behaviors> </component> ...
• 编程方式
Container.Register(Component .For<IMyClass>() .Use<MyClass>(). Proxy<MyClassProxy>(). AddBehavior(new ProfileBehavior()))
代理自动生成
为了简化和自动化代理类的生成过程,我创建了一个简单的工具,它使用 Mono.Cecil* 来发现配置文件中定义的程序集。它是 PhoneCore Framework 源代码和 NuGet 包的一部分。SecureBox 和 PhoneCore Framework 解决方案展示了如何使用该工具。
结果
以下是 SecureBox 项目中方法调用拦截的结果:
如何使用:SecureBox 项目
SecureBox 是一个 Windows Phone 7.5 应用程序,它可以存储你的敏感信息,如账户凭据、电话号码、密码,并防止访问。目前开发尚未完成,但其代码可以帮助理解 PhoneCore Framework 的主要功能。你可以在我之前的文章中找到更多详细信息:A framework for building of WP7 application [3]。
注意:该文章描述的是早期版本的 Framework,有些细节已过时。请参阅 phonecore.codeplex.com [4] 上的发布历史记录和文档。
让我们简要看一下如何使用它。
设置 PhoneCore 框架
添加 PhoneCore Framework 代码的最简单方法是通过 NuGet 包。
下一步是配置框架服务。当前实现要求你创建配置文件。对于 SecureBox:
- application.config - 根配置文件,引用所有其他配置。
- fwk.system.config - 框架初始化配置。它定义了内置框架子系统的使用,例如 DI 容器、日志记录。你可以使用自定义实现和设置替换它们。
- fwk.bootstrap.config - 定义了引导引擎在启动时初始化的默认服务列表。
- securebox.data.config - 存储应用程序数据层特有的设置。
注意:请不要忘记为你的配置文件设置 BuildAction(Build Action)属性为 Content。
接下来,在 App.xaml 中添加以下静态资源:
<Application.Resources>
<core:ApplicationBootstrapper x:Key="Bootstrapper" d:IsDataSource="False" />
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</Application.Resources>
正如你所见,我使用了 MVVMLight 作为 MVVM 框架,并且这里的 ViewModelLocator 已经定义好了。我修改了标准的 ViewModelLocator 逻辑:
public class ViewModelLocator
{
/// <summary>
/// Initializes a new instance of the ViewModelLocator class.
/// </summary>
public ViewModelLocator()
{
}
private IPageMapping _pageMapping;
protected IPageMapping PageMapping
{
get
{
if(_pageMapping == null)
{
var bootstrapper = Application.Current.Resources["Bootstrapper"] as ApplicationBootstrapper;
IContainer container = bootstrapper.GetContainer();
_pageMapping = container.Resolve<IPageMapping>();
}
return _pageMapping;
}
}
#region ViewModel properties
/// <summary>
/// Gets the Startup property which defines the main viewmodel.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public IViewModel Settings
{
get
{
return PageMapping.GetViewModel("Settings");
}
}
...
}
该类的每个属性都返回相应的视图模型,如果它已在配置(pageMapping 节点)中定义,则会自动注册。
<pages>
<page name="Settings">
<uri type="relative" address="/ViewPage/SettingsViewPage.xaml" />
<viewModel type="SecureBox.UI.ViewModel.SettingsViewPageModel, SecureBox.UI" />
</page>
</pages>
另一个重要的事情是创建自定义 bootstrapper 插件,这些插件将在启动时初始化和注册额外的逻辑。SecureBox 包括:
- Init - 读取并设置用户设置。
- DataContext - 初始化并注册数据上下文服务。
- PassGen - 初始化并注册密码生成服务。
完成这些之后,你就可以在项目中启用拦截和其他框架功能了。
摘要
本文旨在展示如何将 AOP 实践引入 WP7 平台。所提供的方法基于自定义 PhoneCore Framework 项目,该项目历史较短,尚未达到生产质量。它是利用我的业余时间开发的,目的是提高我在软件工程学科方面的技能。
链接
- [1] 横切关注点 - 维基百科
- [2] 面向切面编程 - 维基百科
- [3] 构建 WP7 应用程序的框架 - codeproject 文章
- [4] PhoneCore Framework - 源代码,文档
推荐书籍
- (1) “设计模式:可重用面向对象软件元素”,作者:Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
- (2) “Dependency Injection in .NET”,作者:Mark Seemann
- (3) “Applying Domain-Driven Design and Patterns: Using .Net”,作者:Jimmy Nilsson
历史
2012/02/23 - 文章初始状态
更新
* 也可以看看 Roslyn 项目。它似乎是构建 AOP 工具的好东西。