Thought Cloud TDD #3: Train of thought

I’m having fun building Thought Cloud. I hope you are, too.

One thought does not a cloud make. The user needs the ability to create new thoughts.

[TestMethod]
public void CanCreateANewThought()
{
    _cloudViewModel.NewThought.Execute(null);
    Assert.AreEqual(2, _cloudViewModel.Thoughts.Count());
}

To create a new thought, we need access to the Community. We initialized _community and used it to create our Identity and initial Thought. Rather than going back to _community each time we want to add a fact, we’ll let one of the existing facts do the work for us. Let’s have the Identity create a Thought. We can add methods to a fact by declaring a partial class.

public partial class Identity
{
    public Thought NewThought()
    {
        return Community.AddFact(new Thought());
    }
}

Every fact has a private Community property that it can use to create other facts. Remember rule #2: create one fact for each user action. This is how a fact performs actions.

To call this method, CloudViewModel needs an Identity. Let’s add that parameter.

public class CloudViewModel
{
    private Identity _identity;
    private Thought _centralThought; 

    public CloudViewModel(Identity identity, Thought centralThought)
    {
        _identity = identity;
        _centralThought = centralThought;
    } 

    ...
}

And now we can write the NewThought command. It is a property of type ICommand. The unit test executes the command the same way the user would when pressing a button. We’ll implement that interface using MakeCommand from Update Controls.

public ICommand NewThought
{
    get
    {
        return MakeCommand
            .Do(() => _identity.NewThought());
    }
}

Run the test and see if it passes. It doesn’t? Well, what have we missed?

Linked thoughts

Thoughts only form a cloud when you can join them together. The view model is only returning the central thought. We want it to return all related thoughts. So let’s define a link between thoughts.

fact Link {
key:
    Thought* thoughts;
}

A link is identified by the set of thoughts that it connects. By convention, we will only connect two thoughts with a link, but there is no way to enforce that convention in the Factual model. The key is simply a collection of thoughts. Let’s add a LinkTo method to a Thought in a partial class.

public partial class Thought
{
    public Link LinkTo(Thought otherThought)
    {
        return Community.AddFact(new Link(new List<Thought> { this, otherThought }));
    }
}

After we create the new thought, we link it with the central thought of the cloud.

public ICommand NewThought
{
    get
    {
        return MakeCommand.Do(() => _centralThought.LinkTo(_identity.NewThought()));
    }
}

Facts are not just records. They are domain objects with behavior. It’s just that they perform that behavior by creating new facts.

So does it pass yet? No? I guess we need to do one more thing.

Query for related facts

The cloud view model is currently programmed to return only the central thought. It needs to also return all thoughts immediately linked to it. We’ll do that with a query.

fact Thought {
key:
    unique; 

mutable:
    string text; 

query:
    Thought* neighbors {
        Link l : l.thoughts = this
        Thought t : l.thoughts = t
    }
}

The query gets all Links l such that one of the thoughts is this one, and then it gets all Thoughts t such that one of l’s thoughts is t. That’s just set notation for the immediate neighbors. We can return that set from the view model.

public IEnumerable<ThoughtViewModel> Thoughts
{
    get
    {
        return Enumerable.Repeat(new ThoughtViewModel(_centralThought), 1).Union(
            from n in _centralThought.Neighbors
            select new ThoughtViewModel(n));
    }
}

That big fancy linq statement creates a collection having only the central thought view model. Then it unions that with the set of neighbor view models. So we should get two, right? Alas No! The test still fails!

Careful what you query for

If you look closely at the failure reason, this time it failed because we returned too many thoughts. We expected 2, but returned 3. The problem is that the query includes all thoughts of all related links. In other words, the query returns the central thought itself. Let’s filter this out.

public IEnumerable<ThoughtViewModel> Thoughts
{
    get
    {
        return Enumerable.Repeat(new ThoughtViewModel(_centralThought), 1).Union(
            from n in _centralThought.Neighbors
            where n != _centralThought
            select new ThoughtViewModel(n));
    }
}

And now does it pass? Yes!

A fact can relate other facts, just like an associative table in a relational database. In order to find the related facts, just query. Factual uses set notation to define queries, but they work just like relational joins.

Leave a Reply

You must be logged in to post a comment.