ℹ️ This repository is part of my "Patterns of Enterprise Application Architecture" (PoEAA) catalog, based on Martin Fowler's book with the same title. For my full work on the topic, see kaiosilveira/poeaa
A small simple object, like money or a date range, whose equality isn’t based on identity.
A Tag
can be modeled as a value object. Simple tags contain only a text label and are considered equal if the text in one tag matches the text in another.
To implement a value object in C#, we need to override the Equals
and GetHashCode
methods. These methods are used internally by the language to perform object equality comparisons and their defaults are based on comparing memory addresses. As we are only interested in the value equality and not necessarily in the memory address of each object, we can override Equals
to compare a tag's text to some other tag's text. We also need to make sure that GetHashCode
uses the hash code of the tag's text itself, so it's correctly found inside a HashSet
and other hash-code-based data structures.
In a good TDD fashion, unit tests were used to guide this implementation. The rationale for each test and its implementation are described below.
- A
Tag
isn't equal tonull
:
[Fact]
public void TestIsNotEqualNull()
{
var tag = new Tag(text: "work");
Assert.False(tag.Equals(null));
}
- A
Tag
isn't equal another object with a different type:
[Fact]
public void TestIsNotEqualDifferentInstance()
{
var tag = new Tag(text: "work");
var person = new Person(firstName: "Kaio", lastName: "Silveira");
Assert.False(tag.Equals(person));
}
- A
Tag
should be equal another tag with the same title:
[Fact]
public void TestIsEqualAnotherTagWithSameText()
{
var tag1 = new Tag(text: "work");
var tag2 = new Tag(text: "work");
Assert.Equal(tag1, tag2);
}
- A
Tag
should be correctly found inside a hash set:
[Fact]
public void TestIsFoundCorrectlyInsideHashSets()
{
var tag = new Tag(text: "work");
var hashSet = new HashSet<Tag>();
hashSet.Add(tag);
Assert.Single(hashSet);
var result = new Tag(text: "");
Assert.True(hashSet.TryGetValue(tag, out result));
Assert.Equal(tag, result);
}
The full test suite is available at TagTest.cs.
As described above, we need to override the Tag.Equals
and Tag.GetHashCode
methods. The following sections go through these overwrites.
We can apply the logic described in the section above to override the Equals
method:
// class Tag
public override bool Equals(object? obj)
{
var anotherTag = (Tag)obj;
return this.Text == anotherTag.Text;
}
This would make the compiler to implode with many warnings related to possibly null references and unsafe casts, though. To address these concerns, we can apply some defensive programming. The final code would look like this:
// class Tag
public override bool Equals(object? obj)
{
if (obj == null) return false;
try
{
var anotherTag = (Tag)obj;
return this.Text == anotherTag.Text;
}
catch (InvalidCastException)
{
return false;
}
}
To overwrite the GetHashCode
method, we need whether the object has any value in the first place. If that's the case, as it only has a single field, we can return the hash code of its Text
. The code looks like this:
// class Tag
public override int GetHashCode()
{
if (String.IsNullOrEmpty(this.Text)) return 0;
return this.Text.GetHashCode();
}