Value Object: a better implementation
This post is about a better implementation of Value Object.
Value Object: a better implementation
I believe you know what a value object is. Here’s a thorough article about it and what differentiates it from an Entity if you need a refresher: Entity vs Value Object: the ultimate list of differences.
In short, value objects are the building blocks of your domain model. Ideally, you want any concept, however small it is, to be represented by a value object. Got a user with an email? Wrap this email in a separate value object class. Are there several fields that represent an address? Even better, extracting them into a value object would reduce the number of properties the user entity needs to work with.
For many years, I’ve been using an implementation of it which I presented in the DDD in practice course. Here it is:
public abstract class ValueObject<T>
where T : ValueObject<T>
{
public override bool Equals(object obj)
{
var valueObject = obj as T;
if (ReferenceEquals(valueObject, null))
return false;
if (GetType() != obj.GetType())
return false;
return EqualsCore(valueObject);
}
protected abstract bool EqualsCore(T other);
public override int GetHashCode()
{
return GetHashCodeCore();
}
protected abstract int GetHashCodeCore();
public static bool operator ==(ValueObject<T> a, ValueObject<T> b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false;
return a.Equals(b);
}
public static bool operator !=(ValueObject<T> a, ValueObject<T> b)
{
return !(a == b);
}
}
What it does is it handles the repeating parts of the equality members and provides extension points for deriving classes to do the actual comparison and hash code calculation.
Here’s what an Address value object deriving from that base class could look like:
public class Address : ValueObject<Address>
{
public string Street { get; }
public string City { get; }
public string ZipCode { get; }
public Address(string street, string city, string zipCode)
{
Street = street;
City = city;
ZipCode = zipCode;
}
protected override bool EqualsCore(Address other)
{
return Street == other.Street
&& City == other.City
&& ZipCode == other.ZipCode;
}
protected override int GetHashCodeCore()
{
unchecked
{
int hashCode = Street.GetHashCode();
hashCode = (hashCode * 397) ^ City.GetHashCode();
hashCode = (hashCode * 397) ^ ZipCode.GetHashCode();
return hashCode;
}
}
}
As you can see, the base value object class does a pretty good job hiding most of the boilerplate code related to comparison operations. All you need to do is declare two methods. The first one is EqualsCore
whose parameter other
is already strongly typed, so you don’t need to deal with conversion or anything like that. And the second one is GetHashCodeCore
. It doesn’t bring a lot of additional value as you could just override the standard GetHashCode
, but the benefit of having this additional abstract method is that it helps you to not forget about defining it in the deriving classes.
It’s possible to move even more responsibilities to the base ValueObject
class. For example, I saw implementations where the base class used .NET reflection to get the list of all fields and properties in the deriving class and perform the comparison automatically. While it might seem like a good idea as it allows you to reduce the amount of code in Address
even further, I would recommend against it.
The reason why is because such approach fails in two scenarios:
-
It doesn’t work if the value object contains a collection. Collections need special treatment: taking each element and comparing them one by one instead of simply calling
Equals()
on the collection instance. -
It doesn’t work if you need to exclude one of the fields from the comparison. It might be the case that not all properties in a value object created equal. There could be some
Comment
field that doesn’t need to be compared when evaluating equality. At the same time, there’s no way to inform the value object about this. All fields in the derived class will be taken into account and compared against each other.
So, the actual comparison logic in value objects should be implemented consciously, there’s no way you can delegate it to an automatic tool. Hence you need to do a little bit of work in the derived classes by declaring EqualsCore
and GetHashCodeCore
.
However, it is still possible to reduce the amount of work needed. Instead of doing the actual comparison and hash code calculation in the derived classes, you can ask them to enumerate the members that participate in the comparison and do the rest of the work in the base class.
This idea was proposed by a reader of this blog, Steven Roberts. Here’s how the new ValueObject
class looks:
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (GetType() != obj.GetType())
return false;
var valueObject = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Aggregate(1, (current, obj) =>
{
unchecked
{
return current * 23 + (obj?.GetHashCode() ?? 0);
}
});
}
public static bool operator ==(ValueObject a, ValueObject b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false;
return a.Equals(b);
}
public static bool operator !=(ValueObject a, ValueObject b)
{
return !(a == b);
}
}
And this is the updated Address
:
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string ZipCode { get; }
public Address(string street, string city, string zipCode)
{
Street = street;
City = city;
ZipCode = zipCode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return ZipCode;
}
}
As you can see, there’s a lot more going on in the new version of the base class. It doesn’t require you to implement EqualsCore
anymore. Instead, you only need to provide the list of components that comprise the class. The comparison is done by using SequenceEqual()
on the two sets of such components. GetHashCode
is also taken care of: it now takes each component and uses it to build up the resulting hash code.
As for Address
, all it needs to do is this:
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return ZipCode;
}
This approach reduces the amount of repeating code in the derived classes and at the same time doesn’t exhibit the drawbacks of the implementation with the fully automatic comparison. You can still choose which fields you want to take into consideration. And if the value object contains collections, it will work too. All you need to do is enumerate all collection elements alongside with other properties.
This is what you would need to do should you decide to add a list of tenants to the Address
value object:
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string ZipCode { get; }
public List<Tenant> Tenants { get; }
public Address(string street, string city, string zipCode, List<Tenant> tenants)
{
Street = street;
City = city;
ZipCode = zipCode;
Tenants = tenants;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return ZipCode;
foreach (Tenant tenant in Tenants)
{
yield return tenant;
}
}
}
And here’s what to do if you need to override the default Equals()
behavior, for example, implement case-insensitive comparison for a string and specify the precision for a float/double/decimal type:
public class Money : ValueObject
{
public string Currency { get; }
public decimal Amount { get; }
public Money(string currency, decimal amount)
{
Currency = currency;
Amount = amount;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Currency.ToUpper();
yield return Math.Round(Amount, 2);
}
}
Note that GetEqualityComponents()
transforms Currency
into the upper case and rounds Amount
up to two decimal points, so that now it doesn’t matter if you use
var money1 = new Money("usd", 2.2222m);
or
var money2 = new Money("USD", 2.22m);
They would be deemed equal from the domain’s point of view.
Conclusion
I like this implementation a lot as it’s simpler than the one I used previously. All kudos to Steve Roberts.
Related articles
- ← Short-term vs long-term perspective in software development
- Optimistic locking and automatic retry →
Subscribe
Comments
comments powered by Disqus