When I got to this point of the series, I expected to be writing about some interaction testing - checking that our messages triggered the right transitions between screens.

But as I started writing the code, I found it wasn’t hanging together well.

In order to make new tests pass, I was adding new handlers to the existing reducers. Some messages required modifying the ScreenReducer - and others required modifying the WordTutorApplicationReducer.

Nothing wrong with this - it’s essentially what I expected when I made the split.

Except that I found two worrying problems.

The first was that it wasn’t always straightforward to predict which reducer needed modification in order to process a given message. Such a violation of the Principle of Least Surprise didn’t bode well for the future maintainability of the code.

The second was that I found some occasions where I needed to transplant a handler from one reducer to the other. In one case I found that a minor change to a handler required me to completely relocate it from the ScreenReducer to the WordTutorApplicationReducer. Needing to make major changes to accommodate minor improvements, well, again this doesn’t seem like a good situation.

To remedy these problems, I had to revisit the design of our reducers.

We still want to avoid having all the handlers written in a single reducer because that would be a maintenance nightmare. The current split between the two reducers was based on which part of the application state would be affected by the message. This seems to be the key error.

Therefore, I’ve changed things up to instead split the functionality based on the currently active screen. For each distinct screen, we’ll create a dedicated reducer. To make things predictable, we’ll name each one for the associated screen, with the suffix Reducer added.

Our new AddVocabularyWordScreenReducer demonstrates the pattern. We first check to see if the current screen is the expected one, and we exit early if it isn’t. This serves to ensure our reducer only takes action when the expected screen is present.

public WordTutorApplication Reduce(
    IReduxMessage message,
    WordTutorApplication currentState)
{
    if (!(currentState.CurrentScreen is AddVocabularyWordScreen))
    {
        return currentState;
    }

By leaving this decision inside the Reducer, I’ve left the door open for future reducers that work across multiple screens, should we find a good reason to introduce them.

Next, we switch, based on the type of the message.

    switch (message)
    {
        case ModifySpellingMessage m:
            return currentState.UpdateScreen(
                (AddVocabularyWordScreen s) => s.WithSpelling(m.Spelling));

        case ModifyPhraseMessage m:
            return currentState.UpdateScreen(
                (AddVocabularyWordScreen s) => s.WithPhrase(m.Phrase));

        case ModifyPronunciationMessage m:
            return currentState.UpdateScreen(
                (AddVocabularyWordScreen s) => s.WithPronunciation(m.Pronunciation));
    }

If we get a message type that we don’t expect, we just return the status quo.

    return currentState;
}

The other reducer, VocabularyBrowserScreenReducer follows the same structure.

Now the location of each message handler is quite predictable. No need for future maintainers (or us!) to go on a quest across the codebase, looking for the handler required. Instead we can go directly to the right reducer, based on knowledge of which screen is active.

Prior post in this series:
Vocabulary Browser
Next post in this series:
Integration Testing

Comments

blog comments powered by Disqus