Thought Cloud TDD #7: Devoid of thought

I’m writing Thought Cloud for a demo in a couple of weeks. Help me out.

During the last refactoring, we introduced a bug. There just isn’t a test to show it yet. The bug is related to rule number two:

Rule #2: Create one fact for each user action.

In the last refactoring, a single user action created three facts: a cloud, and thought, and a central thought assignment. (You can’t always see it, but an assignment to a mutable field is a new fact.) The problem with creating three facts for a single user action is that we cannot guarantee that all three facts are atomic. There may be situations – particularly when collaborating with another user – where we see only a subset of the facts.

Honor the default state

After each fact that we create, we must assume that the system is in a valid state. In this scenario, we have two intermediate states which must be considered valid:

  • The cloud has been created, but it has no thoughts.
  • The cloud has a thought, but no central thought.

These are the states that arise after the creation of the first and second facts. We must honor the default state of a cloud. Let’s drop the initialization steps from the unit test and see what happens:

[TestInitialize]
public void Initialize()
{
    _community = new Community(new MemoryStorageStrategy())
        .Register<Model.CorrespondenceModel>();

    _identity = _community.AddFact(new Identity("mike"));
    Cloud cloud = _community.AddFact(new Cloud(_identity));
    //Thought thought = _community.AddFact(new Thought(cloud));
    //cloud.CentralThought = thought;
    _cloudViewModel = new CloudViewModel(cloud);
}

Now all three of the tests in this suite fail. They all throw null reference exceptions. The first null reference is in the Thoughts property of the CloudViewModel. The cloud has no central thought. In this situation, we want to simulate a thought to make the tests pass.

public IEnumerable<ThoughtViewModel> Thoughts
{
    get
    {
        Thought centralThought = _cloud.CentralThought;
        if (centralThought == null)
        {
            return Enumerable.Repeat(new ThoughtViewModelSimulated(_cloud), 1)
                .OfType<ThoughtViewModel>();
        }
        else
        {
            return Enumerable.Repeat(new ThoughtViewModelActual(centralThought), 1).Union(
                from n in centralThought.Neighbors
                where n != centralThought
                select new ThoughtViewModelActual(n))
                .OfType<ThoughtViewModel>();
        }
    }
}

If the central thought has not been set, then we return a simulated thought view model. If it has, then we return actual thought view models. Those calls to OfType are there because an IEnumerable of a derived type can’t be converted to an IEnumerable of the base type. Covariance will solve this problem, once it is available in Silverlight.

When the user sets the thought text, the simulated thought view model needs a place to store it. So at that point, it creates a thought.

public class ThoughtViewModelSimulated : ThoughtViewModel
{
    private Cloud _cloud;

    public ThoughtViewModelSimulated(Cloud cloud)
    {
        _cloud = cloud;
    }

    public string Text
    {
        get { return "My thought"; }
        set
        {
            Thought thought = _cloud.NewThought();
            _cloud.CentralThought = thought;
            thought.Text = value;
        }
    }
}

The last null reference exception occurs in the NewThought command of CloudViewModel. It tries to link the new thought to the central thought, but the central thought is null. We must honor this situation, and create a new central thought when needed.

public ICommand NewThought
{
    get
    {
        return MakeCommand.Do(() =>
        {
            Thought thought = _cloud.NewThought();
            Thought centralThought = _cloud.CentralThought;
            if (centralThought == null)
            {
                centralThought = _cloud.NewThought();
                _cloud.CentralThought = centralThought;
            }
            centralThought.LinkTo(thought);
        });
    }
}

Now the CloudViewModel honors the default state of the Cloud fact. It is valid to have a Cloud with no central thought. Just as the ThoughtViewModel returns the simulated text “My thought” when the Text property has not yet been set, the CloudViewModel returns a simulated thought when no central thought has been created. When the user makes a change, the actual Thought is created. But not before.

Leave a Reply

You must be logged in to post a comment.