Aimee.NET - 更快的单元测试和重构 Documents 文件夹






4.13/5 (7投票s)
重构 Lucene.NET 以遵循 .NET 最佳实践和约定,而不是 Java 的编码风格和限制。这是系列文章中的第二篇,将在 www.codeproject.com 发布,从头到尾记录整个过程。
引言
本文对应的代码可在 CodePlex 此处获取。
本系列文章
这是关于重构 Lucene.NET 以遵循 .NET 最佳实践和约定,而不是 Java 的编码风格和限制的系列文章中的第二篇。这些文章将在 The Code Project 发布,从头到尾记录整个过程。第一篇文章可在 Aimee.NET - 重构 Lucene.NET:设置项目获取,主要涉及
- 在 CodePlex 上设置项目,
- 设置 Visual Studio 2010 解决方案,
- 并确定将精力集中在何处,以为 TDD 风格的重构生成一组测试。
在本文中,我将讨论
我曾预计在对 Lucene.NET API 进行重大更改之前,会写 2 或 3 篇文章。不幸的是,正如你将看到的,这并没有发生。因此,Aimee.NET API 非常相似,但有一些不同之处,我认为这些改进了库的可用性。
加速单元测试
你可能还记得上一篇文章,Lucene.NET 测试套件大约需要 45 分钟才能执行。这对于 TDD 重构练习来说是不可接受的。我需要能够进行少量更改,然后快速验证代码是否仍然按预期和测试定义的功能运行。我分析了 NUnit 的结果,以确定哪些测试和/或测试套件花费的时间最多。
根据分析信息,很明显有许多测试实际上是性能和压力类型测试,而不是构成典型单元测试套件的功能测试。这并不是说这些测试不重要,只是它们属于一套单独的测试套件。
幸运的是,大多数这些类型的测试名称中都有 Random 或 Stress 字样。短期内,我已在这些测试上放置了 [Ignore]
属性,并将在我处理性能调优和确保线程稳定性时再回来处理它们。我现在不想这样做,因为我将进行的更改将影响或完全改变代码的工作方式。此时进行任何性能调优都是过早的,而且可能浪费精力。
此外,有几个测试套件在测试设置中包含昂贵的初始化代码,这些代码应该放在测试套件设置中。这是一个简单的更改,将测试执行时间缩短了几分钟。
所有这些活动的结果是,单元测试执行时间从 45 分钟减少到 7 分钟多一点。虽然仍然有点长,但对于我的目的来说是可以接受的。我觉得再做任何额外的努力都不会获得太多收益,于是决定转向更有趣的活动。
转换为枚举
在整个 Lucene.NET 代码中,没有使用 enum
来定义状态和选择,而是使用了模仿 enum
的特殊类。程序员最可能遇到的第一个是 Field.Store
Field.Index
和 Field.TermVector
。它们在 Field
构造函数中用作参数,以指定 Document
字段在索引时的行为。所有这些都派生自 Util.Parameter
类。
Parameter 伪枚举类层次结构

从上图中可以看出,这些类中的大多数都定义在其他类中。这在封闭类和即将成为 enum
的类之间创建了硬依赖。如果我们要最终得到遵循 S.O.L.I.D. 原则的代码,我们希望面向接口编程,而不是实现类。我们希望面向一个 IField
接口编程,该接口具有将 enum
作为参数的方法。接口和 enum
都可以定义在 Core 项目中,用户大部分时间不需要担心实现类。
因此,第一步是用 enum
替换这些类。Field.Store
、Version
和 BooleanClause.Occur
都被转换为 enum
。Field.Index
和 Field.TermVector
类被转换为带有 [Flags]
属性的 enum
。完成此操作并重新编译。Version
和 BooleanClause.Occur
出现了编译错误。这是由于原始类中添加了额外的方法。使用扩展方法解决了这个问题,如下面的 Version
类所示
public static class VersionExtensions
{
public static bool OnOrAfter(this Lucene.Net.Util.Version v,
Lucene.Net.Util.Version other)
{
return (int)v == 0 || (int)v >= (int)other;
}
}
[Serializable]
public enum Version
{
LUCENE_CURRENT = 0,
LUCENE_20 = 2000,
LUCENE_21 = 2100,
LUCENE_22 = 2200,
LUCENE_23 = 2300,
LUCENE_24 = 2400,
LUCENE_29 = 2900
}
接下来,使用 CodeRush 将枚举从其封闭类中提取出来,重命名,并将 Field 枚举提取到单独的文件中。是否将 Operator 和 Occur 枚举从封闭类中提取出来并移动到单独的文件中,将在我们处理这些类时决定。新名称如下表所示
旧名称 | 新名称 | 注释 |
Lucene.Net.Util.Version |
Lucene.Net.Util.Version |
Unchanged |
Lucene.Net.Documents. Field.Index |
Lucene.Net.Documents. FieldIndex |
只需删除最后一个“.” |
Lucene.Net.Documents. Field.Store |
Lucene.Net.Documents. FieldStore |
只需删除最后一个“.” |
Lucene.Net.Documents. Field.TermVector |
Lucene.Net.Documents. FieldTermVector |
只需删除最后一个“.” |
Lucene.Net.QueryParsers. QueryParser.Operator |
Lucene.Net.QueryParsers. QueryParser.Operator |
Unchanged |
Lucene.Net.Search. BooleanClause.Occur |
Lucene.Net.Search. BooleanClause.Occur |
Unchanged |
由于 Field 枚举的重命名,整个项目编译后报告了所需的代码更改。经过一些繁琐的编辑,解决方案(包括单元测试和 contrib)编译并通过了单元测试。
因此,Field
类的 enum
现在看起来像
字段枚举

消除类型别名依赖
在编辑代码时,我注意到 Lucene.NET 代码使用了类型别名,使用语句的形式为
using AliasName = TypeFullName;
在大多数情况下,这些与使用命名空间 using 语句相比没有提供任何好处,只是增加了额外的行。此外,类型别名引入了代码依赖,这可能导致在更改或消除类型名称时进行额外编辑。
这将大多数文件的 using
部分清理成这样
using System;
using Analyzer = Lucene.Net.Analysis.Analyzer;
using Document = Lucene.Net.Documents.Document;
using AlreadyClosedException = Lucene.Net.Store.AlreadyClosedException;
using Directory = Lucene.Net.Store.Directory;
using ArrayUtil = Lucene.Net.Util.ArrayUtil;
using Constants = Lucene.Net.Util.Constants;
using IndexSearcher = Lucene.Net.Search.IndexSearcher;
using Query = Lucene.Net.Search.Query;
using Scorer = Lucene.Net.Search.Scorer;
using Similarity = Lucene.Net.Search.Similarity;
using Weight = Lucene.Net.Search.Weight;
to
using System;
using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Store;
using Lucene.Net.Util;
using Lucene.Net.Search;
重构 Document 类
在查看 Document
类时,我意识到我将无法与 Lucene.NET 保持 API 兼容性。Document
是 Lucene 领域的核心实体之一。Documents
是 Fields
的集合。Lucene.NET 库的用户创建 Documents
,向其添加 Fields
,然后将 Documents
添加到 Index
。稍后,用户将要求 Index
返回符合一组条件的 Documents
列表,并使用某些 Fields
中的值来显示有关返回的 Documents
的信息。
查看 document
类的代码,我很快发现代码是
- 使用 getter 和 setter 方法而不是
Properties
- 使用内部类实现
document
字段列表的Enumerator
- LINQ 风格的查询将大大简化代码。
- 该类具有使用
Field
类的方法和使用Fieldable
接口的方法。我希望它只了解Fieldable
接口。
将方法转换为属性
我实现的第一个 Property
是针对 Boost
值的。GetBoost()
和 SetBoost(float Value)
方法只是修改了一个 private
字段。这使其成为自动属性的绝佳候选。进行此更改,编译并修复错误是直截了当的。
第二个属性是替换 Fields
getter。此 getter 返回一个无类型的 IEnumerator
,它通过返回一个封装了 private
字段集合的枚举器实例来访问已添加到 Document
的 Fields
。我更喜欢使用类型化的枚举器来增加代码安全性。因此,我做了以下更改:
将字段字段的类型从
internal System.Collections.IList fields = new System.Collections.ArrayList();
to
private readonly List<IField> fields = new List<IField>();
这不仅增加了类型安全性,还将实现细节对其他类隐藏起来。
Fields()
方法已更改为属性
public IList<IField> Fields
{
get { return fields; }
}
现在使用 LINQ 查询,各种方法可以变得不那么复杂。例如,Field GetField(string name)
从
public Field GetField(System.String name)
{
for (int i = 0; i < fields.Count; i++)
{
Field field = (Field) fields[i];
if (field.Name().Equals(name))
return field;
}
return null;
}
to
public IField GetField(String name)
{
return fields.FirstOrDefault(f => f.Name == name);
}
两个方法 Fieldable[] GetFieldables(string name)
和 Field[] GetFields(string name)
被一个单独的 IField[] GetFields(string name)
方法取代。
两个方法 Fieldable GetFieldable(string name)
和 Field GetField(string name)
被一个单独的 IField GetField(string name)
方法取代。
为了实现这一点,我将 Fieldable
接口重命名为 IField
,因为我更喜欢我的接口以 I
开头。在理解和修改代码时,区分接口和类非常重要,它们不是一回事。以 I
开头接口的约定使这变得容易得多。我还必须将 string ToString()
方法添加到此接口。

未来的更改可能包括将返回集合的类型从 IList<IField>
更改为 IEnumerator<IField>
。这样会更安全,因为这样就不可能使用 IList
上可用的 Add
或 Remove
方法修改底层集合。
重构 Field 类
重构 Field
相关类的方法与重构 Documents
类似。有许多 getter 和 setter 方法应作为 Properties
实现。这在下面的类图中显示。
原始字段类

我还
- 重构
AbstractField
以利用FieldStore
和FieldTermVector
的位字段值 - 将一些嵌入的
IField
实现提取到单独的文件中,以便于维护。
结果如下所示。
重构的字段类

其他更改
对 Documents 文件夹也做了一些小的更改
NumberTools
已被移除,因为它不再被使用。DateField
应该被移除,但为了索引的向后兼容性,它是必需的。CompressionTools
和DateTools
类被设为static
。
应用 CodeRush 建议的修正
为了更好地利用 CodeRush 的代码分析功能,我“纠正”了 CodeRush 建议的所有警告和错误。其中大部分分为两类:
- 删除冗余命名空间;即,将
System.String
更改为String
- 使用隐式变量。这意味着尽可能使用
var
关键字。我发现这进一步解耦了代码实现,因为在像var myVar = GetMyVarImplementation()
这样的语句中,变量的类型允许我在不破坏某些代码的情况下更改方法的返回类型,如果我正确地面向接口编程,则不会破坏任何代码。即使返回类型更改了类层次结构,如果未访问变量的属性和/或方法,则代码也不会中断。
我学到的东西
首先,在重构时,始终要有一个可用的单元测试套件。即使是简单的更改也可能破坏其他代码,而且和大多数开发人员一样,我大部分的 OSS 开发都是在晚上疲惫时进行的。我犯过错误。单元测试能够快速发现我的错误。
Lucene.NET 代码不仅是 Java 与 .NET 编码风格和实践的问题。还存在一些与 S.O.L.I.D. 原则相关的问题。这在这样古老的代码中是意料之中的。然而,这确实表明 Lucene 团队对于 Java 代码应该,而且我希望对于 V3.X,考虑解决其中一些问题。
后续步骤
我接下来要处理的区域是 Analysis 文件夹。原因有二。首先,它是索引过程中的下一个逻辑步骤,其次是因为我在使用 Lucene.NET 时在该区域遇到了问题。一个大问题是无法修改 StandardAnalyzer 使用的词法分析器。这已使用一个生成 Java 的工具实现,并且语法定义中嵌入了 Java。
在此之后,我可能会处理 Store 文件夹。这确实可以利用 .NET 和 Windows 的许多功能来提高性能。
其余文件夹的可能顺序如下所示,但我正在寻求反馈和潜在的帮助。
- QueryParser
- Util
- 目录
- 搜索
- 消息
我还将把命名空间从 Lucene.NET 重命名为 Aimee.NET。
历史
- 2010 年 12 月 6 日:首次发布