I am writing this question in the context of a DDD application. I found this code online on a number of blogs etc e.g. here and here.
public abstract class ValueObject<T>
where T : ValueObject<T>
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
var valueObject = obj as T;
if (ReferenceEquals(valueObject, null))
return false;
return EqualsCore(valueObject);
}
private bool EqualsCore(ValueObject<T> other)
{
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Aggregate(1, (current, obj) => current * 23 + (obj?.GetHashCode() ?? 0));
}
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);
}
}
}
Every Value Object in my application inherits from it. It means the methods like: Equals
and GetHashCode
are dealt with implicitly. The main problem I see arises if I want to add a collection e.g. array as a member of a Value Object - in this scenario the generic value object .Equals
will not behave as expected as it cannot deal with collections.
Therefore:
- Can this class be tweaked to deal with collections?
- Are there any other downfalls with this approach?
- Is it advisable not to use a generic value class when developing a DDD application?
1 Answer 1
Using the code in your example along with the following article for reference
Representing a collection as a Value Object
Lets start with refactoring the City value object to use the improved ValueObject
public sealed class City : ValueObject<City> {
public string Name { get; }
public bool IsEnabled { get; }
public City(string name, bool isEnabled) {
Name = name;
IsEnabled = isEnabled;
}
protected override IEnumerable<object> GetEqualityComponents() {
yield return Name;
yield return IsEnabled;
}
}
Note how GetEqualityComponents
was implemented. Reflection could also have been used to enumerate the properties as needed
return this.GetType().GetProperties().Select(propInfo => propInfo.GetValue(this, null);
There is room for improvement above and I will leave that as an exercise.
For collections, the GetEqualityComponents
allows for a simplified approach of returning the items used to check equality.
public class CityList : ValueObject<CityList>, IEnumerable<City> {
private List<City> _cities { get; }
public CityList(IEnumerable<City> cities) {
_cities = cities.ToList();
}
protected override IEnumerable<object> GetEqualityComponents() {
return this.OrderBy(c => c.Name);
}
public IEnumerator<City> GetEnumerator() {
return _cities.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
}
Note that since the collection already implements IEnumerable
one simple needs to return the desired enumeration that will be used to calculate equality.
GetEqualityComponents()
proves to be a very convenient approach and really does improve and simplify how the ValueObject
can be implemented. I initially though it to be a premature optimization attempt but having taken some time to review its potential I have changed my initial view of its function.
UPDATE from comments about the potential of using reflection.:
The use of reflection automates and simplifies a repetitive task with a minor performance trade off. Caching the property/field info could help a little but I personally have had no issues with it performance wise. Also using reflection grabs all while using the yield like in the example you get to be selective of which ones to use for equality. You can make an extension method to get the best of both worlds and have options.
public static class ValueObjectExtensions {
/// <summary>
/// Uses reflection to extract all public properties from the target value object
/// </summary>
public static IEnumerable<object> GetComponents<T>(this ValueObject<T> target)
where T : ValueObject<T> {
return target.GetType().GetProperties().Select(propInfo => propInfo.GetValue(target, null));
}
}
and used like
protected override IEnumerable<object> GetEqualityComponents() {
return this.GetComponents(); //<-- calling extension method
}
-
\$\begingroup\$ Thanks. I will test this in the morning at get back to you. Would you use this value object in your systems? - I am trying to establish whether the authors have published a thought exercise or practical tips. I am working towards a completely isolated domain model (with a data model that maps to orm) if that has any bearing. \$\endgroup\$w0051977– w00519772017年12月29日 22:30:17 +00:00Commented Dec 29, 2017 at 22:30
-
\$\begingroup\$ @w0051977 I have used this before in slightly different forms to suit my specific needs. I used the same articles to get a better understanding of the concept. The quoted author also states that they have used this on many projects but he tends to focus more on apply functional programming and adapting it in c#. \$\endgroup\$Nkosi– Nkosi2017年12月29日 22:34:30 +00:00Commented Dec 29, 2017 at 22:34
-
\$\begingroup\$ Do you know what is meant by equalscore (v equals I mean). What is meant by "core"? \$\endgroup\$w0051977– w00519772017年12月30日 10:12:00 +00:00Commented Dec 30, 2017 at 10:12
-
\$\begingroup\$ @w0051977 what do you mean? Can you clarify your last comment. \$\endgroup\$Nkosi– Nkosi2017年12月30日 11:10:21 +00:00Commented Dec 30, 2017 at 11:10
-
\$\begingroup\$ please look at the method name in my original post. Is there a reason it is called: equalscore? (what does the 'core' mean). \$\endgroup\$w0051977– w00519772017年12月30日 11:13:58 +00:00Commented Dec 30, 2017 at 11:13
EqualsCore
to beprotected virtual
to allow derived classes with generic collection arguments to be able to override and determine equality themselves. \$\endgroup\$GetEqualityComponents
but that appears to be premature optimization to me. \$\endgroup\$GetEqualityComponents
implementation by using the collections enumeration for the components. \$\endgroup\$