After creating VocabularyWord
last time, our next step is to create VocabularySet
, a container for many words.
We could take a shortcut and just use a set of words wherever needed - passing around a string (for the name of the set) and an ImmutableHashSet<VocabularyWord>
containing all the words.
There are a couple of reasons I’m not going down this path.
-
It’s almost always worthwhile to define a semantic type to represent core domain concepts. In this case, we’re building an application to tutor spelling words. A list of those words is a pretty foundational concept.
-
We already have two properties for a
VocabularySet
- a name and a list of words - and the chances of adding additional properties later on is high. When you start routinely passing around two or more properties together, that’s a good indication that there’s an underlying type begging to be freed.
Plus, I’ve got a pretty good idea that we’ll have specialised behavour for our set of words. Failing to create a dedicated type up front will just mean that behaviour accumulates in other places.
Properties
For now, we’ll define our VocabularySet
as having two properties:
- A descriptive
Name
that end users can specify so they can tell different lists apart; and - A set of the
Words
contained by the set.
We treat the spelling words as a set because they don’t have any implicit ordering. Note that we couldn’t have used an ImmutableHashSet<>
here if we’d skiped out on implementing Equals()
and GetHashCode()
last time.
Creating a vocabulary set
To create a new VocabularySet
, we’ll follow the established pattern used for immutable collections in .NET and provide a static property called Empty
, along with a suitable private constructor:
Changing the name
To change the name of a VocabularySet
, we declare a With...
method that makes the requested change, returning a new instance of VocabularySet
:
The cached _nameComparer
gives us a convenient way to ensure all our methods that work withName
are mutually consistent. We’ll see this reused later on for Equals()
and GetHashCode()
.
We’ll encounter transformation methods like this quite frequently as we build up our model.
For this method to work, we need a new constructor - one that lets us make a clone of an existing VocabularySet
, but with a particular change.
I’ve used this pattern before, to very good effect. By using a default value of null for each property, we can fall-back to the matching value from original
except where a particular value is provided. Admittedly, this pattern can fall down if null is ever a value we legitimately want to use; hopefully this won’t hit us.
Adding a word
Adding a new word into the set follows a predictable pattern, with most of the code being validation checks of one kind or another.
Note the short circuit - if we’re adding a word that is already present (same spelling, same pronunciation, and so on) then we don’t need to raise an error; instead we just return the existing set.
Removing a word
Similarly, the process for removing a word from the set is also predictable.
Changing a word
We’ll provide two ways to modify a word in the list. First, we have Replace
, which removes one word and substitutes a replacement:
Even here we can short circuit things - if the existing
word and its replacement
are equal, we can skip the rest of the method.
Now that we have Replace
, we can implement Update
, which allows us to make a prescribed change to an existing word.
We identify the word we want to modify by supplying its spelling; once found, we use the supplied transform
to create a new value that’s used as a replacement
.
Checking for spelling
You might have noticed the use of HasSpelling()
to find the word we want, in both Add()
and Update()
. Adding this as a convenience method to VocabularyWord
makes our code more declarative, with the added bonus of ensuring consistency across the application. (If some parts of the app were case sensitive and others case insensitive, subtle bugs would emerge to confuse users.)
We use CurrentCulture
instead of OrdinalCulture
in order to align with the user of the app - if they’re running in a locale with different rules for letter equivalence to New Zealand (or American) English, we want to respect those.
Equality and HashCodes
Implementing IEquatable<VocabularySet>
, is relatively familiar territory, with only the use of SetEquals()
out of the ordinary, if only a little.
The override of Equals(object)
is trivial, so I’ll skip over it.
On the other hand, the override of GetHashCode()
is decidedly non-trivial.
It turns out that ImmutableHashSet<>
doesn’t provide an implementation for GetHashCode()
, likely because it would be difficult to provide a single implementation that always provided good results. This means we need to do it ourselves, and given that the calculation is somewhat expensive, caching the result is a good idea.
Since the enumeration order of a set is not deterministic, we need to explicitly specify the order of words as we iterate through them.
Phew! That’s quite a lot of code for a simple container class. Next time we’ll set up our commandline builds.
Comments
blog comments powered by Disqus