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.
For now, we’ll define our
VocabularySet as having two properties:
- A descriptive
Namethat end users can specify so they can tell different lists apart; and
- A set of the
Wordscontained 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
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
_nameComparer gives us a convenient way to ensure all our methods that work with
Name are mutually consistent. We’ll see this reused later on for
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
Checking for spelling
You might have noticed the use of
HasSpelling() to find the word we want, in both
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.)
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
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.