To begin our little project, we need to create our initial project structure. Once that’s in place, we can create the first class from our application model - VocabularyWord.

Project Creation

It’s frequently the case that a project directory accumulates addition stuff above and beyond the actual source code itself. For this reason, it’s my custom to carefully curate the folder structure of any project, so that things start off tidy and stay that way as the project evolves.

For this project, I’m starting with two subfolders underneath the main project folder:

The src folder is for all the projects that make up deliverable components of our application. In this case, we’re starting with just a single project WordTutor.Core.

The tests folder is for all our test projects, keeping the tests nicely segregated. You can see here the WordTutor.Core.Tests project, which will contain all the unit tests for the Wordtutor.Core project.

BTW, the .vs folder is a working folder for Visual Studio Code, and the .git folder contains our git repository. Both can be ignored.

One thing I’m really appreciating with .NET Core is the new project structure - all of the repetition and boilerplate from the old .csproj files has gone. Check it out - here’s the entire file for WordTutor.Core.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

</Project>

Here, we’re targetting .NET Standard 2.0 for maximum compatibility. Good practice is to target .NET Standard where possible - and the lower the standard you use, the wider the potential for reuse. Though, this is because lower versions are more restrictive, so it’s a balancing act.

The project file for WordTutor.Core.Tests.csproj is a litte more complex, due to project and package references, but it is still far easier to read than the older style:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>

    <IsPackable>false</IsPackable>

    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="FluentAssertions" Version="5.6.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\WordTutor.Core\WordTutor.Core.csproj" />
  </ItemGroup>

</Project>

This time, the project targets an actual runtime, necessary for execution of the unit tests.

Neither project explicitly references any .cs source files - inclusion is now automatic, with a couple of benefits: there is a reduced chance for conflicts when adding additional source files, and this also encourages a tidy codebase where extraneous source files (debris left over from earlier efforts) is cleaned up as it’s made.

Modelling a single word

To encapsulate each individual spelling word, we have the immutable class VocabularlyWord. Here’s an abridged view of the important details:

public class VocabularyWord : IEquatable<VocabularyWord>
{
    public string Spelling { get; }
    public string Pronunciation { get; }
    public string Phrase { get; }

We begin with the essential properties for the word - how to spell it correctly, how to say it correctly (because sometimes a speech engine doesn’t say things the way we expect), and a sample phrase to give the word in context.

    public VocabularyWord(string spelling)
    {
        Spelling = spelling 
            ?? throw new ArgumentNullException(nameof(spelling));
        Pronunciation = string.Empty;
        Phrase = string.Empty;
    }

For simplicity, we initialize a word with just a spelling and then we have some transformation methods that each return a new instance with the required change. This is a little bit wasteful, creating heap debris, but we’re looking at an interactive application here, not a server, so the simplicty of design likely worth the performance cost.

    public VocabularyWord WithSpelling(string spelling)
    {
        return new VocabularyWord(
            this,
            spelling: spelling 
                ?? throw new ArgumentNullException(nameof(spelling)));
    }

    public VocabularyWord WithPronunciation(string pronunciation)
    {
        return new VocabularyWord(
            this,
            pronunciation: pronunciation 
                ?? throw new ArgumentNullException(nameof(pronunciation)));
    }

    public VocabularyWord WithPhrase(string phrase)
    {
        return new VocabularyWord(
            this,
            phrase: phrase 
                ?? throw new ArgumentNullException(nameof(phrase)));
    }

The key that makes these transformation methods work is a private constructor that allows creating of a near-clone of an existing word:

    private VocabularyWord(
        VocabularyWord original,
        string spelling = null,
        string pronunciation = null,
        string phrase = null)
    {
        Spelling = spelling ?? original.Spelling;
        Pronunciation = pronunciation ?? original.Pronunciation;
        Phrase = phrase ?? original.Phrase;
    }

Each parameter has a null default, and if not provided, the matching property from original is used instead. This is possibly because null isn’t a permitted value for any of the properties.

There’s nothing particularly novel about the implementations of Equals() and GetHashCode(), but they’re included for completeness:

    public bool Equals(VocabularyWord word)
    {
        if (word is null)
        {
            return false;
        }

        if (ReferenceEquals(this, word))
        {
            return true;
        }

        return Equals(Spelling, word.Spelling)
            && Equals(Phrase, word.Phrase)
            && Equals(Pronunciation, word.Pronunciation);
    }

    public override bool Equals(object obj)
        => Equals(obj as VocabularyWord);

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 23 + Spelling.GetHashCode();
            hash = hash * 23 + Phrase.GetHashCode();
            hash = hash * 23 + Pronunciation.GetHashCode();
            return hash;
        }
    }
}

The testing of VocabularyWord is pretty straightforward, so I’ll leave it to you to check out the code for yourself.

Next time

In the next post, we’ll look at collecting many words together into a VocabularySet and explore how to transform an immutable collection.

Prior post in this series:
WordTutor Revisted
Next post in this series:
Vocabulary Set

Comments

blog comments powered by Disqus