[Updated March 10, 2009: Important!! While the original content shown below is usable in most cases, there are scenarios wherein it breaks down. The results of GetHashCode should not also be used as the means for expressing object equality. Accordingly, this, and other issues have been resolved and may be viewed within S#arp Architecture's BaseObject.cs class found at http://sharp-architecture.googlecode.com/svn/trunk/src/SharpArch/SharpArch.Core/DomainModel/BaseObject.cs.]
[Updated June 23, 2008: A simplified and more systematic approach to this may be found within S#arp Architecture. While the complexities of the Equals/GetHashCode method are kept away in a base class, you simply need to add an [Identity] attribute over each property in a class which is part of the domain object's "signature." It also supports multiple attributes to be used in the same class for multi-property uniqueness - outside of the ID itself.]
The ability to override GetHashCode is available on every object but is seldom required for POCOs. The Equals method usually provides all of the comparison functionality we'll ever need. But when using an ORM such as NHibernate, GetHashCode takes a more prominent role as it helps NHibernate determine if an object already exists in a collection. Not overriding GetHashCode, or doing so inappropriately, may lead to duplicate objects showing up in collections and/or objects missing altogether. When needed, most people implement both methods and end up with similar code in both. So to ease the burden of managing both of these methods, there's exploitable overlap between Equals and GetHashCode to kill two birds with one stone...no offense to any PETA members out there. (A thanks goes out to Alan Northam whose approach made me aware of the code duplication I previously had in my own approach.)
In my project work, I consider the following to be true when comparing two objects:
- If two objects have IDs, and have the same IDs, then they may be considered equal without examining them further. (I'm assuming ID to be an identity field, or equivalent, in the DB.)
- If two objects have IDs, but have different IDs, than they may be considered not equal without examining them further. E.g., If customer A has an ID of 4 while customer B has an ID of 5, then they are not equal, QED.
- If neither object has an ID or only one of them has an ID, but their "business value signatures" are identical, then they're equal. E.g., customer A has an ID of 4 and a social-security-number of 123-45-6789 while customer B has no ID but also has a social-security-number of 123-45-6789. In this case, customer A and customer B are equal. By "business signatures," I imply a combination of those properties which deem an entity unique, regardless of its ID. (As a side, see Eric Evans' Domain Driven Design for a stellar conversation of value and entity objects. A summary version is also available.)
- If one of them is null, then they are not equal. (Whoo, that was easy.)
With the above considerations in mind, we'll want to write code to make the following unit test pass. Note that Customer takes a company name into its constructor. It also has a settable contact name. The combination of its company name and contact name give Customer its unique business signature.
[Test]
public void CanCompareCustomers() {
Customer customerA = new Customer("Acme");
Customer customerB = new Customer("Anvil");
Assert.AreNotEqual(customerA, null);
Assert.AreNotEqual(customerA, customerB);
customerA.SetIdTo(1);
customerB.SetIdTo(1);
// Even though the signatures are different,
// the persistent IDs were the same. Call me
// crazy, but I put that much trust into IDs.
Assert.AreEqual(customerA, customerB);
Customer customerC = new Customer("Acme");
// Since customerA has an ID but customerC
// doesn't, their signatures will be compared
Assert.AreEqual(customerA, customerC);
customerC.ContactName = "Coyote";
// Signatures are now different
Assert.AreNotEqual(customerA, customerC);
// customerA.Equals(customerB) because they
// have the same ID.
// customerA.Equals(customerC) because they
// have the same signature.
// customerB.DoesNotEquals(customerC) because
// we can't compare their IDs, since
// customerC is transient, and their
// signatures are different.
Assert.AreNotEqual(customerB, customerC);
}
Although some argue against a single object which all other persistable domain objects inherit from, I use one nonetheless and ingeniously call it "DomainObject." (Those are Dr. Evil quotes there.) "DomainObject," in its entirety, contains the following:
public abstract class DomainObject<IdT>
{
/// <summary>
/// ID may be of type string, int,
/// custom type, etc.
/// </summary>
public IdT ID {
get { return id; }
}
public override sealed bool Equals(object obj) {
DomainObject<IdT> compareTo =
obj as DomainObject<IdT>;
return (compareTo != null) &&
(HasSameNonDefaultIdAs(compareTo) ||
// Since the IDs aren't the same, either
// of them must be transient to compare
// business value signatures
(((IsTransient()) || compareTo.IsTransient()) &&
HasSameBusinessSignatureAs(compareTo)));
}
/// <summary>
/// Transient objects are not associated with an
/// item already in storage. For instance, a
/// Customer is transient if its ID is 0.
/// </summary>
public bool IsTransient() {
return ID == null || ID.Equals(default(IdT));
}
/// <summary>
/// Must be implemented to compare two objects
/// </summary>
public abstract override int GetHashCode();
private bool HasSameBusinessSignatureAs(DomainObject<IdT> compareTo) {
return GetHashCode().Equals(compareTo.GetHashCode());
}
/// <summary>
/// Returns true if self and the provided domain
/// object have the same ID values and the IDs
/// are not of the default ID value
/// </summary>
private bool HasSameNonDefaultIdAs(DomainObject<IdT> compareTo) {
return (ID != null &&
! ID.Equals(default(IdT))) &&
(compareTo.ID != null &&
! compareTo.ID.Equals(default(IdT))) &&
ID.Equals(compareTo.ID);
}
/// <summary>
/// Set to protected to allow unit tests to set
/// this property via reflection and to allow
/// domain objects more flexibility in setting
/// this for those objects with assigned IDs.
/// </summary>
protected IdT id = default(IdT);
}
Note that Equals is sealed and cannot be overridden by a DomainObject implementation. I suppose it could be unsealed, but since I put a lot of work into that method, I don't want anyone mucking it up!
Now assume that Customer implements DomainObject. As mentioned above, the combination of its company name and contact name give it its unique signature. So its GetHashCode would be as follows:
public override int GetHashCode() {
return (GetType().FullName + "|" +
CompanyName + "|" +
ContactName).GetHashCode();
}
You'll notice that the start of the method includes the full name of the class type itself. With this in place, two different classes would never return the same signature. (You'll have to reconsider how GetHashCode is implemented to handle inheritance structures; e.g. a Customer and an Employee both inherit from a Person class but Customer and Employee may be equal in some instances...for this, I'd probably only add GetHashCode to the Person class.) Additionally, note that GetHashCode should only contain the "business signature" of the object and not include its ID. Including the ID in the signature would make it impossible to find equality between a transient object and a ! transient object. (Equality for all regardless of transience I say!)
Billy McCafferty
Posted
04-25-2007 3:53 PM
by
Billy McCafferty