Microsoft .NET Framework

将旧 .NET 库迁移到最新目标平台

Josh Lane

Microsoft .NET Framework 的最强大功能之一是针对此平台的丰富第三方开源库和商用库。 事实证明,.NET 开发体系已十分成熟,您不仅有出色的 API 可供从 .NET Framework 自身中选择,而且有数千个非 Microsoft 库可用来处理 HTTP 请求,在桌面应用程序中绘制网格,在文件系统上存储结构化数据,还可以实现介于这两者之间的所有功能。 实际上,快速浏览一下常用 .NET 代码存储库,便可了解 CodePlex 上有 32,000 多个项目,code.msdn.microsoft.com 上有 5,000 多个代码示例,而 NuGet Gallery 上有 10,000 多个独特的软件包!

Windows Phone 8 和 Windows 8 等新软件平台的出现可以给这些经过实践检验的代码库注入新的活力。 多年来一直能够很好地在桌面和服务器上满足您需求的 .NET 库在当今的新环境中同样有用,前提是您愿意付出时间和精力来进行面向这些新平台所需的迁移工作。 过去,这样的任务可能很困难而且非常繁琐,但仍需要仔细明确的规划才能确保成功,Visual Studio 2012 提供的多项功能可最大限度地减少潜在困难并提高跨平台重用的机会。

在本文中,我将探讨实际向上迁移开源 Sterling NoSQL 面向对象数据库 (OODB) 项目期间所遇到的难题。 我将为您简单介绍该库,分享所遇到的迁移障碍以及克服这些障碍的解决方案,然后总结一些模式和最佳做法建议,以供您在自己的库迁移工作中使用。

什么是 Sterling?

Sterling 是一种轻型 NoSQL 数据存储库,可对 .NET 类实例进行快速索引检索。 此外,它还支持更新、删除、备份和还原、截断等操作,但与其他 NoSQL 技术一样,它不提供基于 SQL 语言的通用查询功能。 实际上,“查询”概念由一组不同的有序操作组成:

  • 首先,检索出一个集合,该集合含有预定义的键或映射到延迟加载类实例的索引。
  • 然后,通过索引访问键值集合,对整个可能的结果集执行初步快速筛选。
  • 最后,对当前筛选出来的键/值对使用标准 LINQ to Objects 查询,以进一步缩小搜索结果范围。

很显然,Sterling(以及类似的 NoSQL 数据库)的使用模型不同于 SQL Server 等传统关系数据库所提供的使用模型。 对于初学者来说,缺少正式、截然不同的查询语言似乎格外奇怪。 事实上,鉴于 SQL 查询潜在的复杂性以及由查询和代码间的输入和输出映射而导致的开销,这正是 NoSQL 拥护者希冀的优点。 在 Sterling 等 NoSQL 解决方案中,由于查询和代码是一体的,因此没有映射。

有关 Sterling 的原理及使用的完整介绍不在本文探讨之列(有关其他详细信息,请参见 Jeremy Likness 所著的文章“Sterling for Isolated Storage on Windows Phone 7”,网址是 msdn.microsoft.com/magazine/hh205658),而我将重点介绍一些需要记住的主要优势和折衷方案:

  • 它占用的空间很小(在磁盘上约占 150K),十分适合置入进程内。
  • 它开包即用,处于可序列化的 .NET 类型的标准范围内。
  • 基本功能所需的概念集很小;只需少量的五行 C# 代码即可启动并运行 Sterling。
  • Sterling 不支持传统的数据库功能,如细粒安全性、级联更新和删除、可配置锁定语义、原子性、一致性、隔离性、持久性 (ACID) 保证等等。 如果您需要这些功能,应考虑 SQL Server 等完全关系型数据库引擎。

Sterling 创建者(我的 Wintellect 同事 Jeremy Likness)的设计初衷是让 Sterling 面向多种平台;他为 .NET Framework 4、Silverlight 4 和 5 以及 Windows Phone 7 创建了二进制文件。 因此,考虑将 Sterling 更新为面向 .NET Framework 4.5、Windows Phone 8 和 Windows 应用商店应用程序所需的工作时,我知道体系结构将有助于完成此类工作,但不知道项目具体将需要什么。

我在本文中提出的建议是我将 Sterling 更新为面向 .NET Framework 4.5、Windows Phone 8 和 Windows 应用商店应用程序的直接经验所得。 虽然我的经验之谈中的一些细节特定于 Sterling 项目,但其他许多内容可供 Microsoft 体系中的各种项目和迁移工作借鉴。

挑战和解决方案

为了体现我在将 Sterling 迁移到新目标平台期间所碰到的障碍,我列出了几大类别,我可以将所遇到的问题归纳在其下,并为从事此类项目的任何人提供更全面的指导。

符合发散设计理念第一组潜在问题本质上有些哲理性,但对您将要拓展的总体迁移工作具有实际影响。 问问自己:“我要迁移的库的体系结构和设计与新目标平台的常见模式和使用模型的符合程度如何?”

这不是一个容易解决的问题,因此很找到简单的统一答案。 智能的自定义 Windows 窗体布局管理器可能很难或无法迁移到 Windows Presentation Foundation (WPF)。 API 肯定是有区别的,但主要在于核心设计理念和控制管理概念差别巨大,从长远来看,摆正这两方面的位置肯定会让您出错。 另一个示例是,在 Windows Phone 或 Windows 8 等触控式环境中使用十分适合经典键盘鼠标输入样式的自定义 UI 输入控件可能会带来较差的用户体验。 只有向上迁移代码库的愿望是不够的;旧平台与新平台之间必须存在基础设计兼容性,还要做好打算来协调实际存在的任何细小差异。 对于 Sterling 而言,我就面临着一些这样的难题需要克服。

最突出的设计问题是 Sterling 数据更新同步 API 与库(面向 Windows Phone 8 和 Windows 应用商店应用程序)中此类行为的预期异步性质之间存在不一致。 在多年前设计 Sterling 时,异步 API 十分少见,并且用于创建这些 API 的工具和技术也十分简陋。

下面是预迁移 Sterling 中的典型 Save 方法签名:

object Save<T>(T instance) where T : class, new()

此处值得注意的是,此方法以同步方式执行;也说是说,无论保存实例参数需要花多少时间,都会阻止调用方,以等待该方法完成。 这会导致一系列常见的线程阻塞问题:UI 反应缓慢、显著降低服务器可扩展性等等。

最近几年,用户对反应灵敏的软件设计的期望日益增加;我们都不想容忍 UI 在等待保存操作完成期间被冻结几秒钟。 为了解决这一问题,针对 Windows Phone 8 和 Windows 8 API 等新平台的 API 设计指南要求 Save 等公共库方法应是异步的非阻塞操作。 令人欣慰的是,.NET 基于任务的异步模式 (TAP) 编程模型以及 C# 的 async 和 await 关键字现在已经可以很容易做到这一点。 下面是 Save 的更新签名:

Task<object> SaveAsync<T>(T instance) where T : class, new()

现在,Save 会立即返回,而调用方具有一个可等待的对象 (Task) 来用于最终收集结果(在本例中为新保存实例的唯一关键字)。 在后台完成保存操作期间,不会阻止调用方执行其他工作。

为了清楚起见,我仅在此处显示了方法签名;在后台从同步到异步实现的转换需要进行其他重构工作,并需要切换到每个目标平台的异步文件 API。 例如,Save 的同步实现使用 BinaryWriter 写入文件系统:

using ( BinaryWriter instanceFile = _fileHelper.GetWriter( instancePath ) ) {   instanceFile.Write( bytes ); }

但由于 BinaryWriter 不支持异步语义,因此我对此代码进行了重构以使用适合每个目标平台的异步 API。 例如,图 1 显示 SaveAsync 如何查找 Sterling Windows Azure 表存储驱动程序。

图 1 SaveAsync 如何查找 Sterling Windows Azure 表存储驱动程序

using ( var stream = new MemoryStream() ) {   using ( var writer = new BinaryWriter( stream ) )   {     action( writer );   }   stream.Position = 0;   var entity = new DynamicTableEntity( partitionKey, rowKey )   {     Properties = new Dictionary<string, EntityProperty>     {       { "blob", new EntityProperty( stream.GetBuffer() ) }     }   };   var operation = TableOperation.InsertOrReplace( entity );   await Task<TableResult>.Factory.FromAsync(     table.BeginExecute, table.EndExecute, operation, null ); }

 

我仍使用 BinaryWriter 将离散值写入内存中的流,但随后使用了 Windows Azure DynamicTableEntity 以及 CloudTable.BeginExecute 和 .EndExecute 以异步方式将该流的字节数组内容存储在 Windows Azure 表存储服务中。要实现 Sterling 的异步数据更新行为,必须进行其他一些类似的更改。要点:在实现此类迁移重新设计目标所需的若干步骤中,表面层级的 API 重构可能仅是其中的第一步。请相应地估计好您的工作任务和工作量,并首先切实了解此类更改是否是完全合理的目标。

事实上,我设计 Sterling 的经历中就出现了这样不切实际的目标。Sterling 的核心设计特征是所有存储操作均使用标准 .NET 数据合约序列化 API 和扩展来处理强类型数据。这十分适用于 Windows Phone 和 .NET 4.5 客户端以及基于 C# 的 Windows 应用商店应用程序。不过,在 Windows Store HTML5 和 JavaScript 客户端领域中,没有强类型这一概念。在与 Likness 共同进行一些研究和讨论后,我确定没有什么简单的方法可以使 Sterling 可用于这些客户端,于是我选择了放弃将其作为可支持的方案。这样潜在的不一致性当然必须具体情况具体分析,但您应清楚它们可能会出现并切实了解您的方案。

跨目标平台共享代码:我所面临的下一大挑战是我时不时遇到的这样一个问题:如何跨多个项目共享常用代码?

跨项目标识并共享常用代码是一个成熟的策略,可最大限度地缩小上市时间并减少下游维护的麻烦。多年以来,我们一直在 .NET 中这么做;典型模式是定义一个公用程序集并从多个使用者项目引用该程序集。另一个常用方法是 Visual Studio 中的“添加为链接”功能,该功能授予在多个项目之前共享单个主源文件的能力,如图 2 所示。

The Visual Studio 2012 Add As Link Feature图 2:Visual Studio 2012 的“添加为链接”功能

即使在今天,这些方案也十分适用,前提是使用者项目全部面向同一基础平台。但是,当您希望跨多个平台开放常用功能(就像示例中的 Sterling 一样)时,为此类代码创建单个公用程序集会变成一个沉重的开发负担。创建和维护多个生成目标将是不可避免的任务,这会增加项目配置和生成过程的复杂性。使用预处理器指令(#if、#endif 等)有条件地为特定生成配置包含平台特定的行为几乎是必要的,这使代码更难以阅读、浏览和理解。在此类配置负担上浪费精力会偏离通过代码解决实际问题的主要目标。

幸好,Microsoft 预计到更简单跨平台开发的需求,并开始在 .NET Framework 4 中添加了一个名为可移植类库 (PCL) 的新功能。利用 PCL,您可以有选择性地面向 .NET Framework、Silverlight 和 Windows Phone 以及 Windows 应用商店和 Xbox 360 的多个版本,所有这些均可从单个 Visual Studio .NET 项目中实现。当您选择 PCL 项目模板时,Visual Studio 会自动确保代码仅使用每个所选目标平台上存在的库。这样就无需使用繁重的预处理器指令和多个生成目标。另一方面,该功能对可从库调用哪些 API 设定了一些限制;我将稍后说明如何绕开此类限制。有关 PCL 功能和用法的详细信息,请参见“使用 .NET Framework 实现跨平台开发” (msdn.microsoft.com/library/gg597391)。

PCL 十分适合我实现 Sterling 跨平台目标。我能够将 90% 以上的 Sterling 代码库重构到一个公共可用 PCL 中,而无需从 .NET Framework 4.5、Windows Phone 8 和 Windows 8 进行修改。这对于项目的长期适用性带来巨大的优势。

有关单元测试项目的简要说明:截至今天,没有适合单元测试代码的 PCL。创建此类 PCL 的主要障碍在于,缺少可跨多个平台工作的单个统一单元测试框架。考虑到这一事实,对于 Sterling 单元测试,我为 .NET 4.5、Windows Phone 8 和 Windows 8 各自定义了一个测试项目;.NET 4.5 项目包含测试代码的单独副本,而另两个项目使用前面提到的“添加为链接”方法共享测试代码。每个平台项目都引用该平台唯一的测试框架程序集;幸运的是,每个程序集的命名空间和类型名称都相同,因此将在所有测试项目中按原样编译相同的代码。有关此功能的工作原理,请参见 GitHub (bit.ly/YdUNRN) 上的最新 Sterling 代码库。

利用平台特定的 API:虽然 PCL 在创建统一跨平台代码库方面十分有用,但它也带来一些难题:如何使用无法从 PCL 代码调用的平台特定 API?一个最好的示例是,我所提到的异步代码重构;尽管 .NET 4.5 和 Windows 应用商店应用程序特别拥有丰富的强大 API 可供选择,但没有任何 API 可从 PCL 调用。鱼和熊掌能兼得吗?

实际,只需一些工作,便可做到这一点。具体思路是,在 PCL 中定义一个或多个接口,为无法直接调用的平台特定行为建模,然后按照这些接口抽象实现基于 PCL 的代码。然后,在单独的平台特定库中,为每个接口提供实现。最后,在运行时创建 PCL 类型的实例以完成某一任务,从而插入适合当前目标平台的特定接口实现。使用抽象可以使 PCL 代码始终与平台细节分离。

如果这一方法听起来有点熟悉,它应当:我在这里介绍的模式称为控制反转 (IoC),这是一种实现模块化去耦合和隔离的可靠软件设计技术。您可在 bit.ly/13VBTpQ 上详细了解 IoC。

在迁移 Sterling 期间,我使用这一方法解决了多个 API 不兼容性问题。多数有问题的 API 来自 System.Reflection 命名空间。具有讽刺意味的是,每个目标平台都公开了 Sterling 所需的所有反射功能,每个目标平台又都具有自己的细微古怪和差异之处,使得无法在 PCL 中统一支持这些平台。因此,需要使用这一基于 IoC 的技术。您可在 bit.ly/13FtFgO 上找到我为 Sterling 避开这些问题而定义的 C# 接口抽象。

一些常规建议

至此,我已概述了针对 Sterling 的迁移策略,我将再执行一个简单的步骤,然后考虑如何将经验教训应用到常见情况。

首先,对于使用 PCL 功能,我不再占用篇幅来强调这一点。PCL 实现了跨平台开发的巨大成功,它提供了足够的配置灵活性,可最大限度地满足需求。如果您要面向多个平台向上迁移现有库(甚至编写新的库),应使用 PCL。

其次,对重构工作作出预判以符合更新的设计目标。换句话说,不要期望代码迁移是一个十分简单的机械过程,只是将一个 API 调用替换为另一个 API 调用。您需要进行的更改完全有可能会更深入表面层级以下,并可能需要更改编写原始代码库时所做出的一个或多个核心假设。如要避免对下游产生重大影响,可对现有代码所做的整体改动则有实际的限制;您将必须自行决定该行代码位于何处,何时以及是否可以跨过。一个人所做的迁移对于另一个来说意味着一个全新的项目。

最后,不要丢弃现有的模式和设计方法工具箱。我已经演示如何对 Sterling 使用 IoC 原理和依赖关系注入来利用平台特定的 API。毫无疑问,其他类似的方法也十分有效。在重构现有代码以实现新用途时,策略 (bit.ly/Hhms)、适配器 (bit.ly/xRM3i)、模板方法 (bit.ly/OrfyT) 和外层 (bit.ly/gYAK9) 等经典软件设计模式会十分有用。

全新的领域

我的工作的最终成果是,Sterling NoSQL 实现在三个目标平台 .NET Framework 4.5、Windows 8 和 Windows Phone 8 上完全正常运行。令人满意的是,看到 Sterling 在我的 Surface 平台电脑和 Nokia Lumia 920 手机等基于 Windows 的最新设备上运行。

Sterling 项目位于 Wintellect GitHub 网站 (bit.ly/X5jmUh),其中包含完全迁移的源代码以及每个平台的单元测试和示例项目。其中还包含使用 Windows Azure 表存储的 Sterling 驱动程序模型的实现。我会邀请您克隆 GitHub 存储库并探讨我在本文中概述的模式和设计方案;希望这些可作为一个十分有用的起点,帮助您自己开展类似的工作。

还要记住,不要丢弃旧代码 … 而是将其迁移!

Josh Lane 是亚特兰大 Wintellect LLC 的资深顾问。他从事了 15 年在 Microsoft 平台上构建、设计和生成软件方面的工作,并成功地提供了各种技术解决方案,从呼叫中心网站到自定义 JavaScript 编译器等等。他十分喜欢应对通过软件创造有意义商业价值的挑战。您可以通过 jlane@wintellect.com 与他联系。

衷心感谢以下技术专家对本文的审阅:Jeremy Likness (Wintellect)