Thursday, August 15, 2013

Stable Hash Codes and Abstract Base Classes

I like to use abstract base classes for my value objects. One of my most used is for identities:

public abstract class Identity<TId>
    where TId : Identity<TId>
{
    private readonly string _prefix;
    public abstract string Value { get; protected set; }

    /* ... */

    protected Identity(string value, string prefix)
        : this(prefix)
    {
        Value = value;
    }

    // So we can deserialize and have access to the prefix
    protected Identity(string prefix)
    {
        _prefix = prefix; 
    }
}

Since the identities are value objects, we need to make sure that any two identities of the same type with the same values are considered equal.

public abstract class Identity<TId> : IEquatable<TId>
    where TId : Identity<TId>
{
    private readonly string _prefix;
    public abstract string Value { get; protected set; }

    public bool Equals(TId other)
    {
        if (ReferenceEquals(null, other)) return false;
        return ReferenceEquals(this, other) || Equals(Value, other.Value);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, other)) return false;
        return ReferenceEquals(this, obj) || Equals(obj as TId);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hash = 17;
            hash += hash * 23 + Value.GetHashCode();
            hash += hash * 23 + GetType().GetHashCode();
            return hash;
        }
    }

    public override string ToString() { return string.Format("{0}/{1}", _prefix, Value); }

    protected Identity(string value, string prefix)
        : this(prefix)
    {
        Value = value;
    }

    // So we can deserialize and have access to the prefix
    protected Identity(string prefix)
    {
        _prefix = prefix; 
    }
}

Almost...

This works for most situations but, recently, I found out that there's a little bit of a bug with the implementation of GetHashCode(). The overall algorithm is pretty sound for most cases, minus one little piece: GetType().GetHashCode(). The way I found out this doesn't work is by having to persist the hash between uses of an app. Bringing the app down and back up caused GetType().GetHashCode() to return a different value. Whoops!

(The reason for GetType().GetHashCode() was so that two identities of different types with the same .Value; would return different hashes.)

I switched it up, for now, to use GetType().Name. Fixed the issue for me.

public abstract class Identity<TId> : IEquatable<TId>
    where TId : Identity<TId>
{
    /* ... */
    public override int GetHashCode()
    {
        unchecked
        {
            var hash = 17;
            hash += hash * 23 + Value.GetHashCode();
            hash += hash * 23 + GetType().Name.GetHashCode();
            return hash;
        }
    }
    /* ... */
}

And, to wrap up the use of this class, here's a sample of how to use it (fully serializable, too):

[DataContract(Namespace = "my-namespace")]
public abstract class ProductId : Identity<ProductId>
{
    [DataMember(Order = 1)]
    public override string Value { get; protected set; }

    public ProductId(long id)
        : base(id.ToString(CultureInfo.InvariantCulture), "product") { }

    private ProductId()
        : base("product") { }
}

No comments:

Post a Comment