Skip to content

Commit

Permalink
Modified the hashcode multipler used by BaseObject.cs and Entity.cs t…
Browse files Browse the repository at this point in the history
…o be 31 based on information found at http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/ and other sources.  Modified Entity.cs to include the object's type in the hashcode calculation, in addition to its Id, for persistent objects.  Added a couple of tests to prove all of this working, including one using LINQ's Intersect with BaseObjectEqualityComparer.cs.
  • Loading branch information
wmccafferty committed May 25, 2009
1 parent e67e66e commit b58ad06
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 37 deletions.
18 changes: 9 additions & 9 deletions src/SharpArch/SharpArch.Core/DomainModel/BaseObject.cs
Expand Up @@ -31,11 +31,11 @@ public abstract class BaseObject
}

/// <summary>
/// Used to provide the hashcode identifier of an object using the signature
/// This is used to provide the hashcode identifier of an object using the signature
/// properties of the object; although it's necessary for NHibernate's use, this can
/// also be useful for business logic purposes and has been included in this base
/// class, accordingly. Since it is recommended that GetHashCode change infrequently,
/// if at all, in an object's lifetime; it's important that properties are carefully
/// if at all, in an object's lifetime, it's important that properties are carefully
/// selected which truly represent the signature of an object.
/// </summary>
public override int GetHashCode() {
Expand All @@ -51,7 +51,7 @@ public abstract class BaseObject
object value = property.GetValue(this, null);

if (value != null)
hashCode = (hashCode * RANDOM_PRIME_NUMBER) ^ value.GetHashCode();
hashCode = (hashCode * HASH_MULTIPLIER) ^ value.GetHashCode();
}

if (signatureProperties.Any())
Expand Down Expand Up @@ -136,12 +136,12 @@ public abstract class BaseObject
private static Dictionary<Type, IEnumerable<PropertyInfo>> signaturePropertiesDictionary;

/// <summary>
/// This particular magic number is often used in GetHashCode computations but is actually
/// quite random. Resharper uses 397 as its number when overrideing GetHashCode, so it
/// either started there or has a deeper and more profound history than 42.
///
/// And yes, I know it's ironic having a constant with the word "random" in its name.
/// To help ensure hashcode uniqueness, a carefully selected random number multiplier
/// is used within the calculation. Goodrich and Tamassia's Data Structures and
/// Algorithms in Java asserts that 31, 33, 37, 39 and 41 will produce the fewest number
/// of collissions. See http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
/// for more information.
/// </summary>
private const int RANDOM_PRIME_NUMBER = 397;
private const int HASH_MULTIPLIER = 31;
}
}
Expand Up @@ -6,10 +6,17 @@ namespace SharpArch.Core.DomainModel
/// Provides a comparer for supporting LINQ methods such as Intersect, Union and Distinct.
/// This may be used for comparing objects of type <see cref="BaseObject" /> and anything
/// that derives from it, such as <see cref="Entity" /> and <see cref="ValueObject" />.
///
/// WARNING NOTE: Microsoft decided that set operators such as Intersect, Union and
/// Distinct should not use the IEqualityComparer's Equals() method when comparing objects,
/// but should instead use IEqualityComparer's GetHashCode() method. Two instances of
/// <see cref="BaseObject"/> will only return the same HashCode if they have at least one
/// [DomainSignature] decorated property and those
/// expect
/// </summary>
public class BaseObjectEqualityComparer : IEqualityComparer<BaseObject>
public class BaseObjectEqualityComparer<T> : IEqualityComparer<T> where T : BaseObject
{
public bool Equals(BaseObject firstObject, BaseObject secondObject) {
public bool Equals(T firstObject, T secondObject) {
// While SQL would return false for the following condition, returning true when
// comparing two null values is consistent with the C# language
if (firstObject == null && secondObject == null)
Expand All @@ -21,7 +28,7 @@ public class BaseObjectEqualityComparer : IEqualityComparer<BaseObject>
return firstObject.Equals(secondObject);
}

public int GetHashCode(BaseObject obj) {
public int GetHashCode(T obj) {
return obj.GetHashCode();
}
}
Expand Down
32 changes: 23 additions & 9 deletions src/SharpArch/SharpArch.Core/DomainModel/Entity.cs
Expand Up @@ -64,8 +64,6 @@ public abstract class EntityWithTypedId<IdT> : ValidatableObject, IEntityWithTyp

#endregion

private int? cachedHashcode;

#region Entity comparison support

/// <summary>
Expand Down Expand Up @@ -105,19 +103,24 @@ public abstract class EntityWithTypedId<IdT> : ValidatableObject, IEntityWithTyp
HasSameObjectSignatureAs(compareTo);
}

/// <summary>
/// Simply here to keep the compiler from complaining.
/// </summary>
public override int GetHashCode() {
if(cachedHashcode.HasValue)
return cachedHashcode.Value;
if(IsTransient())
{

if (IsTransient()) {
cachedHashcode = base.GetHashCode();
return cachedHashcode.Value;
}
else {
unchecked {
// It's possible for two objects to return the same hash code based on
// identically valued properties, even if they're of two different types,
// so we include the object's type in the hash calculation
int hashCode = GetType().GetHashCode();
cachedHashcode = (hashCode * HASH_MULTIPLIER) ^ Id.GetHashCode();
}
}

return Id.GetHashCode();
return cachedHashcode.Value;
}

/// <summary>
Expand All @@ -130,6 +133,17 @@ public abstract class EntityWithTypedId<IdT> : ValidatableObject, IEntityWithTyp
Id.Equals(compareTo.Id);
}

private int? cachedHashcode;

/// <summary>
/// To help ensure hashcode uniqueness, a carefully selected random number multiplier
/// is used within the calculation. Goodrich and Tamassia's Data Structures and
/// Algorithms in Java asserts that 31, 33, 37, 39 and 41 will produce the fewest number
/// of collissions. See http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
/// for more information.
/// </summary>
private const int HASH_MULTIPLIER = 31;

#endregion
}
}
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework.SyntaxHelpers;
using SharpArch.Testing;
using System.Linq;

namespace Tests.SharpArch.Core.DomainModel
{
Expand All @@ -12,15 +13,15 @@ public class BaseObjectEqualityComparerTests
{
[Test]
public void CanCompareNulls() {
BaseObjectEqualityComparer comparer = new BaseObjectEqualityComparer();
BaseObjectEqualityComparer<BaseObject> comparer = new BaseObjectEqualityComparer<BaseObject>();
Assert.That(comparer.Equals(null, null));
Assert.That(comparer.Equals(null, new ConcreteBaseObject()), Is.False);
Assert.That(comparer.Equals(new ConcreteBaseObject(), null), Is.False);
}

[Test]
public void CannotSuccessfullyCompareDifferentlyTypedObjectsThatDeriveFromBaseObject() {
BaseObjectEqualityComparer comparer = new BaseObjectEqualityComparer();
BaseObjectEqualityComparer<BaseObject> comparer = new BaseObjectEqualityComparer<BaseObject>();

ConcreteBaseObject object1 = new ConcreteBaseObject() {
Name = "Whatever"
Expand All @@ -34,7 +35,7 @@ public class BaseObjectEqualityComparerTests

[Test]
public void CanCompareBaseObjects() {
BaseObjectEqualityComparer comparer = new BaseObjectEqualityComparer();
BaseObjectEqualityComparer<BaseObject> comparer = new BaseObjectEqualityComparer<BaseObject>();

ConcreteBaseObject object1 = new ConcreteBaseObject() {
Name = "Whatever"
Expand All @@ -50,7 +51,7 @@ public class BaseObjectEqualityComparerTests

[Test]
public void CanCompareValueObjects() {
BaseObjectEqualityComparer comparer = new BaseObjectEqualityComparer();
BaseObjectEqualityComparer<BaseObject> comparer = new BaseObjectEqualityComparer<BaseObject>();

ConcreteValueObject object1 = new ConcreteValueObject() {
Name = "Whatever"
Expand All @@ -66,13 +67,13 @@ public class BaseObjectEqualityComparerTests

[Test]
public void CanCompareEntitiesWithNoDomainSignatureProperties() {
BaseObjectEqualityComparer comparer = new BaseObjectEqualityComparer();
BaseObjectEqualityComparer<BaseObject> comparer = new BaseObjectEqualityComparer<BaseObject>();

ConcreteEntityWithNoDomainSignatureProperties object1 = new ConcreteEntityWithNoDomainSignatureProperties() {
Name = "Whatever"
};
ConcreteEntityWithNoDomainSignatureProperties object2 = new ConcreteEntityWithNoDomainSignatureProperties() {
Name = "Whatever"
Name = "asdf"
};
Assert.That(comparer.Equals(object1, object2), Is.False);

Expand All @@ -83,7 +84,7 @@ public class BaseObjectEqualityComparerTests

[Test]
public void CanCompareEntitiesWithDomainSignatureProperties() {
BaseObjectEqualityComparer comparer = new BaseObjectEqualityComparer();
BaseObjectEqualityComparer<Entity> comparer = new BaseObjectEqualityComparer<Entity>();

ConcreteEntityWithDomainSignatureProperties object1 = new ConcreteEntityWithDomainSignatureProperties() {
Name = "Whatever"
Expand All @@ -101,6 +102,36 @@ public class BaseObjectEqualityComparerTests
Assert.That(comparer.Equals(object1, object2));
}

[Test]
public void CanBeUsedByLinqSetOperatorsSuchAsIntersect() {
IList<ConcreteEntityWithDomainSignatureProperties> objects1 = new List<ConcreteEntityWithDomainSignatureProperties>();
ConcreteEntityWithDomainSignatureProperties object1 = new ConcreteEntityWithDomainSignatureProperties() {
Name = "Billy McCafferty",
};
EntityIdSetter.SetIdOf<int>(object1, 2);
objects1.Add(object1);

IList<ConcreteEntityWithDomainSignatureProperties> objects2 = new List<ConcreteEntityWithDomainSignatureProperties>();
ConcreteEntityWithDomainSignatureProperties object2 = new ConcreteEntityWithDomainSignatureProperties() {
Name = "Jimi Hendrix",
};
EntityIdSetter.SetIdOf<int>(object2, 1);
objects2.Add(object2);
ConcreteEntityWithDomainSignatureProperties object3 = new ConcreteEntityWithDomainSignatureProperties() {
Name = "Doesn't Matter since the Id will match and the presedence of the domain signature will go overridden",
};
EntityIdSetter.SetIdOf<int>(object3, 2);
objects2.Add(object3);

Assert.That(objects1.Intersect(objects2,
new BaseObjectEqualityComparer<ConcreteEntityWithDomainSignatureProperties>()).Count(),
Is.EqualTo(1));
Assert.AreEqual(objects1.Intersect(objects2,
new BaseObjectEqualityComparer<ConcreteEntityWithDomainSignatureProperties>()).First(), object1);
Assert.AreEqual(objects1.Intersect(objects2,
new BaseObjectEqualityComparer<ConcreteEntityWithDomainSignatureProperties>()).First(), object3);
}

private class ConcreteBaseObject : BaseObject
{
protected override IEnumerable<PropertyInfo> GetTypeSpecificSignatureProperties() {
Expand Down
Expand Up @@ -2,6 +2,7 @@
using SharpArch.Core.DomainModel;
using NUnit.Framework.SyntaxHelpers;
using SharpArch.Testing;
using System.Collections.Generic;

namespace Tests.SharpArch.Core.DomainModel
{
Expand Down Expand Up @@ -138,22 +139,32 @@ public void Two_persistent_entities_with_different_domain_signature_and_equal_id
}

[Test]
[Ignore("This behavior has changed, no longer an entity changes his hashcode on the fly when domain signature changes")]
public void CanComputeConsistentHashWithDomainSignatureProperties() {
public void KeepsConsistentHashThroughLifetimeOfTransientObject() {
ObjectWithOneDomainSignatureProperty object1 = new ObjectWithOneDomainSignatureProperty();
int defaultHash = object1.GetHashCode();
int initialHash = object1.GetHashCode();

object1.Age = 13;
int domainSignatureAffectedHash = object1.GetHashCode();
Assert.AreNotEqual(defaultHash, domainSignatureAffectedHash);
object1.Name = "Foo";

Assert.AreEqual(initialHash, object1.GetHashCode());

object1.Age = 14;
Assert.AreEqual(initialHash, object1.GetHashCode());
}

// Name property isn't a domain signature property and shouldn't affect the hash
[Test]
public void KeepsConsistentHashThroughLifetimeOfPersistentObject() {
ObjectWithOneDomainSignatureProperty object1 = new ObjectWithOneDomainSignatureProperty();
EntityIdSetter.SetIdOf<int>(object1, 1);
int initialHash = object1.GetHashCode();

object1.Age = 13;
object1.Name = "Foo";
Assert.AreEqual(domainSignatureAffectedHash, object1.GetHashCode());

// Changing a domain signature property will impact the hash generated
Assert.AreEqual(initialHash, object1.GetHashCode());

object1.Age = 14;
Assert.AreNotEqual(domainSignatureAffectedHash, object1.GetHashCode());
Assert.AreEqual(initialHash, object1.GetHashCode());
}

[Test]
Expand Down Expand Up @@ -404,6 +415,7 @@ private class PhoneBeingNotDomainObjectButWithOverriddenEquals : PhoneBeingNotDo
// Even though the "business value signatures" are different, the persistent Ids
// were the same. Call me crazy, but I put that much trust into persisted Ids.
Assert.That(object1, Is.EqualTo(object2));
Assert.That(object1.GetHashCode(), Is.EqualTo(object2.GetHashCode()));

ObjectWithIntId object3 = new ObjectWithIntId() { Name = "Acme" };

Expand Down

0 comments on commit b58ad06

Please sign in to comment.