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:
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:
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:
And this is the updated Address:
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:
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:
I like this implementation a lot as it’s simpler than the one I used previously. All kudos to Steve Roberts.