Thought Cloud TDD #11: Conflict detection

The Thought Cloud application is just about ready to release. Get the code now, or watch this space for a live deployment.

An occasionally-connected client application works off line as well as on. While off line, it queues changes that the user makes. It synchronizes those changes when the connection is reestablished. So the question arises: what happens if someone made a conflicting change while I was off line?

Correspondence performs conflict detection. Conflict resolution is up to your application.

Every mutable field is represented in code with the Disputable<T> type. This type implicitly converts to T. The Text property of a Thought is a mutable string.

fact Thought {
key:
    unique;
    publish Cloud cloud;

mutable:
    string text;

    ...
}

This generates to a Disputable<string>, which the view model treats as a regular string.

public class ThoughtViewModelActual : ThoughtViewModel
{
    ...

    public string Text
    {
        get { return _thought.Text ?? "My thought"; }
        set
        {
            _thought.Text = value;
            _container.EditThought = null;
        }
    }
}

Disputable<T> has a Value property which can be used in place of implicit type conversion. It also has two extra properties:

  • InConflict – true when a conflict is detected.
  • Candidates – the collection of possible values.

With these two properties, your application can present conflicts to the user.

[TestMethod]
public void ConflictsAreDetected()
{
    Cloud cloud_Mike = MikeSharesCloudWithRussell();
    Thought thought_Mike = cloud_Mike.NewThought();
    cloud_Mike.CentralThought = thought_Mike;
    thought_Mike.Text = "Initial value";

    Synchronize();

    Thought thought_Russell = _russellsIdentity.SharedClouds.Single().CentralThought;
    thought_Mike.Text = "Mike's change";
    thought_Russell.Text = "Russell's change";

    Synchronize();

    Assert.IsTrue(thought_Mike.Text.InConflict);
    Assert.IsTrue(thought_Mike.Text.Candidates.Contains("Mike's change"));
    Assert.IsTrue(thought_Mike.Text.Candidates.Contains("Russell's change"));
    Assert.IsFalse(thought_Mike.Text.Candidates.Contains("Initial value"));

    Assert.IsTrue(thought_Russell.Text.InConflict);
    Assert.IsTrue(thought_Russell.Text.Candidates.Contains("Mike's change"));
    Assert.IsTrue(thought_Russell.Text.Candidates.Contains("Russell's change"));
    Assert.IsFalse(thought_Russell.Text.Candidates.Contains("Initial value"));
}

To resolve a conflict, simply assign the field to the desired value. It does not necessarily have to be one of the candidates.

[TestMethod]
public void ConflictsCanBeResolved()
{
    Cloud cloud_Mike = MikeSharesCloudWithRussell();
    Thought thought_Mike = cloud_Mike.NewThought();
    cloud_Mike.CentralThought = thought_Mike;
    thought_Mike.Text = "Initial value";

    Synchronize();

    Thought thought_Russell = _russellsIdentity.SharedClouds.Single().CentralThought;
    thought_Mike.Text = "Mike's change";
    thought_Russell.Text = "Russell's change";

    Synchronize();

    thought_Mike.Text = "Mike's resolution";

    Synchronize();

    Assert.IsFalse(thought_Mike.Text.InConflict);
    Assert.AreEqual("Mike's resolution", thought_Mike.Text.Value);

    Assert.IsFalse(thought_Russell.Text.InConflict);
    Assert.AreEqual("Mike's resolution", thought_Russell.Text.Value);
}

You can leave conflict resolution to the user. Expose these properties through the data model to give the user a visual indicator of the conflict. When they set the value, they will resolve the conflict.

Leave a Reply

You must be logged in to post a comment.