65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.13/5 (7投票s)

2010 年 12 月 8 日

Apache

10分钟阅读

viewsIcon

30123

downloadIcon

271

重构 Lucene.NET 以遵循 .NET 最佳实践和约定,而不是 Java 的编码风格和限制。这是系列文章中的第二篇,将在 www.codeproject.com 发布,从头到尾记录整个过程。

引言

本文对应的代码可在 CodePlex 此处获取。

本系列文章

  1. Aimee.NET - 重构 Lucene.NET:设置项目
  2. Aimee.NET - 更快的单元测试和重构 Documents 文件夹

这是关于重构 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 的结果,以确定哪些测试和/或测试套件花费的时间最多。

根据分析信息,很明显有许多测试实际上是性能和压力类型测试,而不是构成典型单元测试套件的功能测试。这并不是说这些测试不重要,只是它们属于一套单独的测试套件。

幸运的是,大多数这些类型的测试名称中都有 RandomStress 字样。短期内,我已在这些测试上放置了 [Ignore] 属性,并将在我处理性能调优和确保线程稳定性时再回来处理它们。我现在不想这样做,因为我将进行的更改将影响或完全改变代码的工作方式。此时进行任何性能调优都是过早的,而且可能浪费精力。

此外,有几个测试套件在测试设置中包含昂贵的初始化代码,这些代码应该放在测试套件设置中。这是一个简单的更改,将测试执行时间缩短了几分钟。

所有这些活动的结果是,单元测试执行时间从 45 分钟减少到 7 分钟多一点。虽然仍然有点长,但对于我的目的来说是可以接受的。我觉得再做任何额外的努力都不会获得太多收益,于是决定转向更有趣的活动。

转换为枚举

在整个 Lucene.NET 代码中,没有使用 enum 来定义状态和选择,而是使用了模仿 enum 的特殊类。程序员最可能遇到的第一个是 Field.Store Field.IndexField.TermVector。它们在 Field 构造函数中用作参数,以指定 Document 字段在索引时的行为。所有这些都派生自 Util.Parameter 类。

Parameter 伪枚举类层次结构

Parameters Class Hierarchy

从上图中可以看出,这些类中的大多数都定义在其他类中。这在封闭类和即将成为 enum 的类之间创建了硬依赖。如果我们要最终得到遵循 S.O.L.I.D. 原则的代码,我们希望面向接口编程,而不是实现类。我们希望面向一个 IField 接口编程,该接口具有将 enum 作为参数的方法。接口和 enum 都可以定义在 Core 项目中,用户大部分时间不需要担心实现类。

因此,第一步是用 enum 替换这些类。Field.StoreVersionBooleanClause.Occur 都被转换为 enumField.IndexField.TermVector 类被转换为带有 [Flags] 属性的 enum。完成此操作并重新编译。VersionBooleanClause.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 现在看起来像

字段枚举

Field Class enums

消除类型别名依赖

在编辑代码时,我注意到 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 领域的核心实体之一。DocumentsFields 的集合。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 字段集合的枚举器实例来访问已添加到 DocumentFields。我更喜欢使用类型化的枚举器来增加代码安全性。因此,我做了以下更改:

将字段字段的类型从

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() 方法添加到此接口。

Document Class Refactored

未来的更改可能包括将返回集合的类型从 IList<IField> 更改为 IEnumerator<IField>。这样会更安全,因为这样就不可能使用 IList 上可用的 AddRemove 方法修改底层集合。

重构 Field 类

重构 Field 相关类的方法与重构 Documents 类似。有许多 getter 和 setter 方法应作为 Properties 实现。这在下面的类图中显示。

原始字段类

Original Field Classes

我还

  • 重构 AbstractField 以利用 FieldStoreFieldTermVector 的位字段值
  • 将一些嵌入的 IField 实现提取到单独的文件中,以便于维护。

结果如下所示。

重构的字段类

Refactored Field Classes

其他更改

Documents 文件夹也做了一些小的更改

  • NumberTools 已被移除,因为它不再被使用。
  • DateField 应该被移除,但为了索引的向后兼容性,它是必需的。
  • CompressionToolsDateTools 类被设为 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 的许多功能来提高性能。

其余文件夹的可能顺序如下所示,但我正在寻求反馈和潜在的帮助。

  1. QueryParser
  2. Util
  3. 目录
  4. 搜索
  5. 消息

我还将把命名空间从 Lucene.NET 重命名为 Aimee.NET。

历史

  • 2010 年 12 月 6 日:首次发布
© . All rights reserved.