问题
I often work with classes that represent entities produced from a factory.
To enable easy testing of my factories easily I usually implement IEquatable<T>
, whilst also overriding GetHashCode
and Equals
(as suggested by the MSDN).
For example; take the following entity class which is simplified for example purposes. Typically my classes have a bunch more properties. Occasionally there is a also collection, which in the Equals
method I check using 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();
}
}
This means I can then do simple unit tests like so (assuming the constructor is tested elsewhere).
[TestMethod]
public void TestFactory()
{
Product expected = new Product("James");
Product actual = ProductFactory.BuildJames();
Assert.AreEqual(expected, actual);
}
However this raises a number of questions.
GetHashCode
isn't actually used but I've spent time implementing it.- I rarely actually want to use
Equals
in my actual application beyond testing. - I spend more time writing more tests to ensure the
Equals
actually works correctly. - I now have another three methods to maintain, e.g. add a property to the class, update methods.
But, this does give me a very neat TestMethod
.
Is this an appropriate use of IEquatable
, or should I take another approach?
回答1:
Whether this is a good idea or not really depends on what kind of type your factory creates. There are two kinds of types:
Types with value semantics (value types for short) and
Types with reference semantics (reference types for short.)
In C# it is common to use struct
for value types and class
for reference types, but you do not have to, you may use class
for both. The point is that:
Value types are meant to be small, usually immutable, self-contained objects whose main purpose is to contain a certain value, while
Reference types are objects that have complex mutable state, possibly references to other objects, and non-trivial functionality, i.e. algorithms, business logic, etc.
If your factory is creating a value type, then sure, go ahead and make it IEquatable
and use this neat trick. But in most cases, we don't use factories for value types, which tend to be rather trivial, we use factories for reference types, which tend to be rather complex, so if your factory is creating a reference type, then really, these kinds of objects are only meant to be compared by reference, so adding Equals()
and GetHashCode()
methods is anywhere from misleading to wrong.
Take a hint from what happens with hash maps: the presence of Equals()
and GetHashCode()
in a type generally means that you can use an instance of this type as a key in a hash map; but if the object is not an immutable value type, then its state may change after it has been placed in the map, in which case the GetHashCode()
method will start evaluating to something else, but the hash map will never bother re-invoking GetHashCode()
in order to re-position the object in the map. The result in such cases tends to be chaos.
So, the bottom line is that if your factory creates complex objects, then you should perhaps take a different approach. The obvious solution is to invoke the factory and then check each property of the returned object to make sure they are all as expected.
I could perhaps propose an improvement to this, though beware that I just thought of it, I have never tried it, so it may and may not turn out to be a good idea in practice. Here it is:
Your factory presumably creates objects that implement a particular interface. (Otherwise, what's the point of having a factory, right?) So, you could in theory stipulate that newly created instances of objects that implement this interface should have certain properties initialized to a particular set of values. This would be a rule imposed by the interface, so you could have some function tied to the interface which checks whether this is true, and this function could even be parametrized with some hint as to expect different initial values under different circumstances.
(Last I checked, in C# a method tied to an interface was usually an extension method; I do not remember off the top of my head whether C# allows static methods to be part of an interface, or whether the designers of C# have yet added to the language something as neat and elegant as the default interface methods of Java.)
So, with an extension method, it could perhaps look like this:
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" ) );
回答2:
For test code you could use a reflection based equality, something like: Comparing object properties in c#
Many testing libraries provide such a utility, this way you don't have to change the design of your production code to suit the tests.
来源:https://stackoverflow.com/questions/33988487/should-i-be-using-iequatable-to-ease-testing-of-factories