Suppose I have two classes that implement the same interface, as in the example in C#
below:
public interface ICommonInterface
{
string Text { get; }
int Count { get; }
}
public class ImplementationA: ICommonInterface
{
// Implementation A
}
public class ImplementationB: ICommonInterface
{
// Implementation B
}
I want to define equality for implementations of ITargetOccurence
as ITargetOccurence.Text
and ITargetOccurence.Count
being both equal.
What is a good way of defining such equality, and why?
Possible Solutions I Have Considered:
1) Implementing a Static Method Helper For Equality
I could make equality of interfaces something that can be accessed via a static helper class, such as:
public static class CommonInterfaceHelper
{
public static bool AreEqual(ICommonInterface A, ICommonInterface B)
{
// ...
}
}
This does not make use of any equality pattern in the language, though.
2) Overriding Equality Operator In The Classes
I could do something similar to the implementation suggested in this question: https://stackoverflow.com/questions/22101703/overriding-equals-in-c-sharp-interface-implementation
public class ImplementationA: ICommonInterface
{
public override bool Equals(object obj)
{
ICommonInterface other = obj as ICommonInterface;
// ...
}
}
// Same thing for ImplementationB
I believe that equality returning true
for instances of another class is counter intuitive, though.
3) Changing ICommonInterface
to a struct
Instead of making ICommonInterface
an interface, it could perhaps be a struct
that has a better defined equality. The resulting implementation could be something like this:
public struct CommonEnum
{
string Text;
int Count;
}
public class ImplementationA
{
// Implementation A
public static implicit operator CommonEnum(ImplementationA imp)
{
CommonEnum commonEnum = new CommonEnum();
commonEnum.Text = imp.Text;
commonEnum.Count = imp.Count;
};
}
// Same thing for ImplementationB
This unties the ImplementationA
and ImplementationB
, though.
4) Argue that interfaces (ICommonInterface
) should not have a equality constraint
Interfaces are just specifications on what methods a class should have. In that case, one could argue that ICommonInterface
should not have a "equality constraint" and that one should check each method individually if comparing equality.
5) Make Equality a Method Defined by the Interface
With the same reasoning as (4) one could argue that if equality was to be expected, the Interface should define:
public interface ICommonInterface
{
string Text { get; }
int Count { get; }
bool IsEqual(ICommonInterface other);
}
This also does not make use of the equality patterns the language provides, though.
Is there any standard way of defining this equality, by following a given set of good practices or principles?
For clarification regarding acceptance criteria for answers, I believe that an answer to this question should be either:
- An argument that this is an "opinion based" subject and that there are no set of good practices or principles that indicate one of the given approaches is preferred.
- An argument that one of the approaches is preferred by following a given set of good practices or principles, with some links or explanation of what principles were used.
1 Answer 1
There is no single answer to your question: It strongly depends on what you are modelling/abstracting. In short, most of your proposed solutions are conditionally valid! Some comments with respect to your proposed solutions
Option 1: Your idea behind this proposition is, probably, to allow a custom equality to be defined, which is irrespective of the implementation. This is OK, but it's not the best you could do. A static equality method does not help you that much because it is not "composable" (so to speak). Prefer an
IEqualityComparer
. Why is this more useful? Because you can easily "plug" it into aDictionary
orHashSet
and now, your equality method actually does something without the need for further "adaptations". In short, it is easier to re-use anEqualityComparer
than a static method (classes are higher-level abstractions than methods).Option 2: This is almost the most reasonable case, because in the vast majority of cases, equality is an implementation detail. However, you don't cast the object to the interface. You cannot decide for another type, only your own type. If you want to be "fair" (in the OOP sense),
Equals
for aType
should only matter to the sameType
. Otherwise, you are breaking the least astonishment principle, but also, your entire model (you make decisions for other types inside your type). This is going to rob you of flexibility and maintainability if you expect more implementations of the interface. Someone decides to implement the interface and, suddenly, finds out his instances are "replaceable" by instances of some other type. In short, yourEquals
method could end up being invoked in places you might forget.
Notably, the Remarks
section of that last link states:
The Default property checks whether type T implements the System.IEquatable interface and, if so, returns an EqualityComparer that uses that implementation. Otherwise, it returns an EqualityComparer that uses the overrides of Object.Equals and Object.GetHashCode provided by T.
Option 3: This is a rather extreme measure just to control equality. You are probably hitting a small nail with a large hammer there, all the more so if the operator is implicit. Instead of commenting on why this is not a truly good idea overall, let me remind you that, apart from how your solutions are designed, you should also think about how they are going to be used. This option requires someone to know the additional detail that, when they want to check for equality, they have to create two
CommonEnum
objects and compare them instead. This is something that needs more explaining than the simple self-evidentEquals
method.Options 4 & 5: I can not advise because I don't know what these interfaces actually are. For example, if I am designing interfaces for coordinates (for some reason), I could argue that points must have some equality because it is part of their definition. However, there are still better options than this, so equality should probably not be part of the interface because it expands it for no good reason. I mean, you may end up not needing to use it at all, so why define it up front. There is no thing such as "best practices" here because you are designing both the model, and the subtleties. In any case, if you decide to force a method, just make your interface extend
IEquatable<T>
instead. This makes it more versatile when it comes to the patterns of the language you refer to.
The TL;DR
version is the simple suggestion that, using external EqualityComparers
, there is virtually no scenario you cannot serve, let alone some of the most important ones (e.g. HashSet
s and Dictionary
s) directly out-of-the-box. Pair that with the fact that you don't have to even touch your interfaces and classes, and there you have it. Versatile tailor-made equality, without touching the definitions. Thinking that IEquatable
might make your objects less intuitive (because it is an additional layer of information that a developer-reader has to process, during which a lot of "why"s can occur), I am hard-pressed to conclude that IEqualityComparer
would be the least-astonishing practice, ergo the typically most useful option.
P.S.: As a bonus, I have a real-world scenario for you, where IEquatable
is the option to go with! You have built a huge codebase and, thus far, you have gotten by with the default equality, which is based on reference equality. That means that EqualityComparer
s you simply did not bother too much with. So far so good... Now you have to change an immutable object, by creating another immutable object in-place (with one different property) and replace the old one. But everywhere in your vast mess of code, wherever you had tied that object to various things (i.e. used it as a key), you are going to lose all these relations and you cannot really bother to update all places where your object was used as a key, in order to update the key with the new immutable instance. This will necessitate too much searching.
In this scenario, you realize that, while it's very late, it's not too late. You can now implement IEquatable
properly, and suddenly, your two instances of the immutable object actually end up creating the same key (unless, of course, you changed a property that defines equality, which you didn't). Now you can replace the immutable instance and all your Dictionaries and HashSets, etc., will not break the key-value relations!
IEquatable<T>
for your concrete implementations (i.e. your classes). See docs.microsoft.com/en-us/dotnet/api/…