A Value Object cannot live on its own without an Entity. An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. VALUE OBJECTS are instantiated to represent elements of the design that we care about only for what they are, not who or which they are. The following is a C# base class that gives all the Framework Design Guidelines requirements, as well as the Domain Driven Design requirements, without any additional logic from concrete types.
using System;
using System.Collections.Generic;
using System.Reflection;
// ============================================================================
// Generic Value Object
//
// ## Value Object Requirements
//
// In the Domain Driven Design space, a Value Object:
//
// - Has no concept of an identity
// - Two different instances of a Value Object with the same values are
// considered equal
// - Describes a characteristic of another thing
// - Is immutable
//
// Unfortunately, in nearly all cases I've run in to, we can't use Value Types
// in .NET to represent Value Objects. Value Types (struct) have some size
// limitations (~16 bytes or less), which we run into pretty quickly. Instead,
// we can create a Reference Type (class) with Value Type semantics, similar to
// the .NET String type. The String type is a Reference Type, but exhibits
// Value Type semantics, since it is immutable. For a Reference Type to exhibit
//
// ## Value Type semantics, it must:
//
// - Be immutable
// - Override the Equals method, to implement equality instead of identity,
// which is the default
//
// Additionally, Framework Design Guidelines has some additional requirements I
// must meet:
//
// - Provide a reflexive, transitive, and symmetric implementation of Equals
// - Override GetHashCode
// - Implement IEquatable<T>
// - Override the equality operators
//
// ## Generic Implementation
//
// What I wanted was a base class that would give me all of the Framework Design
// Guidelines requirements as well as the Domain Driven Design requirements,
// without any additional logic from concrete types. Here's what I ended up
// with:
//
// Author: Jimmy Bogard
// http://grabbagoft.blogspot.com/2007/06/generic-value-object-equality.html
// https://lostechies.com/joeocampo/2007/04/23/a-discussion-on-domain-driven-design-value-objects/
// ============================================================================
public abstract class ValueObject<T> : IEquatable<T> where T : ValueObject<T>
{
public virtual bool Equals(T other)
{
if (other == null)
{
return false;
}
var t = GetType();
var otherType = other.GetType();
if (t != otherType)
{
return false;
}
var fields = t.GetFields(
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public);
foreach (var field in fields)
{
var value1 = field.GetValue(other);
var value2 = field.GetValue(this);
if (value1 == null)
{
if (value2 != null)
{
return false;
}
}
else if (!value1.Equals(value2))
{
return false;
}
}
return true;
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var other = obj as T;
return Equals(other);
}
public override int GetHashCode()
{
var fields = GetFields();
var startValue = 17;
var multiplier = 59;
var hashCode = startValue;
foreach (var field in fields)
{
var value = field.GetValue(this);
if (value != null)
{
hashCode = hashCode * multiplier + value.GetHashCode();
}
}
return hashCode;
}
private IEnumerable<FieldInfo> GetFields()
{
var t = GetType();
var fields = new List<FieldInfo>();
while (t != typeof (object))
{
fields.AddRange(t.GetFields(
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public)
);
t = t.BaseType;
}
return fields;
}
public static bool operator ==(ValueObject<T> x, ValueObject<T> y)
{
return x.Equals(y);
}
public static bool operator !=(ValueObject<T> x, ValueObject<T> y)
{
return !(x == y);
}
}
// ============================================================================
// The Tests
//
// Just for completeness, I'll include the set of NUnit tests I used to write
// this class up. I think the tests describe the intended behavior well enough.
// ============================================================================
[TestFixture]
public class ValueObjectTests
{
private class Address : ValueObject<Address>
{
private readonly string _address1;
private readonly string _city;
private readonly string _state;
public Address(string address1, string city, string state)
{
_address1 = address1;
_city = city;
_state = state;
}
public string Address1
{
get { return _address1; }
}
public string City
{
get { return _city; }
}
public string State
{
get { return _state; }
}
}
private class ExpandedAddress : Address
{
private readonly string _address2;
public ExpandedAddress(string address1, string address2, string city, string state)
: base(address1, city, state)
{
_address2 = address2;
}
public string Address2
{
get { return _address2; }
}
}
[Test]
public void AddressEqualsWorksWithIdenticalAddresses()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address1", "Austin", "TX");
Assert.IsTrue(address.Equals(address2));
}
[Test]
public void AddressEqualsWorksWithNonIdenticalAddresses()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address2", "Austin", "TX");
Assert.IsFalse(address.Equals(address2));
}
[Test]
public void AddressEqualsWorksWithNulls()
{
Address address = new Address(null, "Austin", "TX");
Address address2 = new Address("Address2", "Austin", "TX");
Assert.IsFalse(address.Equals(address2));
}
[Test]
public void AddressEqualsWorksWithNullsOnOtherObject()
{
Address address = new Address("Address2", "Austin", "TX");
Address address2 = new Address("Address2", null, "TX");
Assert.IsFalse(address.Equals(address2));
}
[Test]
public void AddressEqualsIsReflexive()
{
Address address = new Address("Address1", "Austin", "TX");
Assert.IsTrue(address.Equals(address));
}
[Test]
public void AddressEqualsIsSymmetric()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address2", "Austin", "TX");
Assert.IsFalse(address.Equals(address2));
Assert.IsFalse(address2.Equals(address));
}
[Test]
public void AddressEqualsIsTransitive()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address1", "Austin", "TX");
Address address3 = new Address("Address1", "Austin", "TX");
Assert.IsTrue(address.Equals(address2));
Assert.IsTrue(address2.Equals(address3));
Assert.IsTrue(address.Equals(address3));
}
[Test]
public void AddressOperatorsWork()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address1", "Austin", "TX");
Address address3 = new Address("Address2", "Austin", "TX");
Assert.IsTrue(address == address2);
Assert.IsTrue(address2 != address3);
}
[Test]
public void DerivedTypesBehaveCorrectly()
{
Address address = new Address("Address1", "Austin", "TX");
ExpandedAddress address2 = new ExpandedAddress("Address1", "Apt 123", "Austin", "TX");
Assert.IsFalse(address.Equals(address2));
Assert.IsFalse(address == address2);
}
[Test]
public void EqualValueObjectsHaveSameHashCode()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address1", "Austin", "TX");
Assert.AreEqual(address.GetHashCode(), address2.GetHashCode());
}
[Test]
public void TransposedValuesGiveDifferentHashCodes()
{
Address address = new Address(null, "Austin", "TX");
Address address2 = new Address("TX", "Austin", null);
Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
}
[Test]
public void UnequalValueObjectsHaveDifferentHashCodes()
{
Address address = new Address("Address1", "Austin", "TX");
Address address2 = new Address("Address2", "Austin", "TX");
Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
}
[Test]
public void TransposedValuesOfFieldNamesGivesDifferentHashCodes()
{
Address address = new Address("_city", null, null);
Address address2 = new Address(null, "_address1", null);
Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
}
[Test]
public void DerivedTypesHashCodesBehaveCorrectly()
{
ExpandedAddress address = new ExpandedAddress("Address99999", "Apt 123", "New Orleans", "LA");
ExpandedAddress address2 = new ExpandedAddress("Address1", "Apt 123", "Austin", "TX");
Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode());
}
}