我应该使用 IEquatable 来简化工厂测试吗?

Should I be using IEquatable to ease testing of factories?

我经常使用 classes 代表工厂生产的实体。 为了方便地测试我的工厂,我通常实现 IEquatable<T>,同时覆盖 GetHashCodeEquals(如 MSDN 所建议)。

例如;以下面的实体 class 为示例目的进行了简化。通常我的 classes 有更多的属性。偶尔还有一个集合,在 Equals 方法中我使用 SequenceEqual.

检查
public class Product : IEquatable<Product>
{
    public string Name
    {
        get;
        private set;
    }

    public Product(string name)
    {
        Name = name;
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }

        Product product = obj as Product;

        if (product == null)
        {
            return false;
        }
        else
        {
            return Equals(product);
        }
    }

    public bool Equals(Product other)
    {
        return Name == other.Name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

这意味着我可以像这样进行简单的单元测试(假设构造函数在别处进行了测试)。

[TestMethod]
public void TestFactory()
{
    Product expected = new Product("James");

    Product actual = ProductFactory.BuildJames();

    Assert.AreEqual(expected, actual);
}

然而,这引发了一些问题。

  1. GetHashCode 并没有实际使用,但我已经花时间实现了它。
  2. 除了测试之外,我很少真正想在我的实际应用程序中使用 Equals
  3. 我花更多时间编写更多测试以确保 Equals 实际工作正常。
  4. 我现在有另外三种维护方法,例如将 属性 添加到 class,更新方法。

但是,这确实给了我一个非常整洁的 TestMethod

这是对 IEquatable 的适当使用,还是我应该采用其他方法?

这是否是个好主意实际上取决于您的工厂创建的类型。有两种类型:

  • 具有值语义的类型(简称值类型)和

  • 具有引用语义的类型(简称引用类型。)

在 C# 中,通常对值类型使用 struct,对引用类型使用 class,但您不必这样做,您可以对两者都使用 class。重点是:

  • 值类型是小型的,通常是不可变的,自包含的对象,其主要目的是包含某个值,而

  • 引用类型是具有复杂可变状态、可能引用其他对象和重要功能(即算法、业务逻辑等)的对象

如果您的工厂正在创建一个值类型,那么当然,继续制作它 IEquatable 并使用这个巧妙的技巧。但在大多数情况下,我们不会为值类型使用工厂,它们往往相当琐碎,我们为引用类型使用工厂,这往往相当复杂,所以如果你的工厂正在创建引用类型,那么实际上,这些各种对象只能通过引用进行比较,因此添加 Equals()GetHashCode() 方法可能会误导甚至错误。

从散列映射的情况中得到提示:类型中 Equals()GetHashCode() 的存在通常意味着您可以使用此类型的实例作为散列映射中的键;但是如果对象不是不可变值类型,那么它的状态可能会在它被放入映射后发生变化,在这种情况下 GetHashCode() 方法将开始评估其他东西,但哈希映射永远不会打扰重新-调用 GetHashCode() 以便在地图中重新定位对象。这种情况下的结果往往是混乱。

所以,最重要的是,如果您的工厂创建了复杂的对象,那么您也许应该采取不同的方法。显而易见的解决方案是调用工厂,然后检查返回对象的每个 属性 以确保它们都符合预期。

我也许可以提出对此的改进,但请注意,我只是想到它,我从未尝试过,所以在实践中它可能会也可能不会成为一个好主意。在这里:

您的工厂可能会创建实现特定接口的对象。 (否则,拥有工厂有什么意义,对吗?)因此,理论上您可以规定新创建的实现此接口的对象实例应该将某些属性初始化为一组特定的值。这将是接口强加的规则,因此您可以将一些函数绑定到接口来检查这是否为真,并且这个函数甚至可以用一些提示进行参数化,以在不同情况下期望不同的初始值。

(上次我检查过,在 C# 中,绑定到接口的方法通常是 扩展方法 ;我不记得是否C# 允许静态方法成为接口的一部分,或者 C# 的设计者是否已经向语言中添加了与 Java 的默认接口方法一样简洁优雅的东西。)

因此,使用扩展方法,它可能看起来像这样:

public boolean IsProperlyInitializedInstance( this IProduct self, String hint )
{
    if( self.Name != hint )
        return false;
    //more checks here
    return true;
}

IProduct product = productFactory.BuildJames();
Assert.IsTrue( product.IsProperlyInitializedInstance( hint:"James" ) );

对于测试代码,您可以使用基于反射的相等性,例如:Comparing object properties in c#

许多测试库都提供了这样的实用程序,这样您就不必更改生产代码的设计来适应测试。