Fork me on GitHub

TomasMikula/blog

How to ensure consistency of your observable state

In this post, I would like to make an appeal to JavaFX developers, especially library creators, to provide stronger consistency guaranties of their observable objects.

The problem

When implementing an object with two or more observable properties that should satisfy a certain invariant (i.e. they are not completely independent), it is often tricky to ensure that the invariant holds all the time the client code is able to query the values of the properties. The invariant often breaks for a brief moment when updating property values: a listener of the property to be updated first can observe the old state of the property to be updated next. As a consequence, the client code needs to be more defensive (i.e. longer and less clear) to handle potential inconsistencies.

We demonstrate the issue on TextArea. It has (among others) these properties: caretPosition, anchor, selection. There is a natural relationship that should hold between these three properties:

  • selection = (anchor, caretPosition), if anchor ≤ caretPosition;
  • selection = (caretPosition, anchor), otherwise.

Stated in code, it is

selection.equals(IndexRange.normalize(anchor, caretPosition))

So let’s write a program that tests the above invariant:

TextArea area = new TextArea();
area.caretPositionProperty().addListener(obs -> checkConsistency(area));
area.anchorProperty().addListener(obs -> checkConsistency(area));
area.selectionProperty().addListener(obs -> checkConsistency(area));

void checkConsistency(TextArea area) {
    IndexRange selection = area.getSelection();
    int caretPosition = area.getCaretPosition();
    int anchor = area.getAnchor();
    assert selection.equals(IndexRange.normalize(anchor, caretPosition));
}

Try the full runnable version. Don’t forget to enable assertions. Run the program and type a letter to the text area. You will get an AssertionError, which means the tested invariant has been broken.

Can we fix it?

Yes!

InhiBeans, a subpackage of ReactFX, provides property and binding implementations that allow you to update their value or invalidate them without notifying the listeners right away. You can update a number of properties and only then notify the listeners. By the time a listener of property p queries the value of property q, both properties will have been updated and thus consistent.

Here is a sketch of how one could fix the above inconsistency in TextArea:

  1. Switch the implementation of caretPosition, anchor and selection properties for their counterpart from org.reactfx.inhibeans.
  2. Replace any selectionChangingOperation() on TextArea (such as typing, moving the caret, …) by

    try(
        Guard g1 = caretPosition.block();
        Guard g2 = anchor.block();
        Guard g3 = selection.block()
    ) {
        selectionChangingOperation();
    }
    

    or, more concisely

    Guardian.combine(caretPosition, anchor, selection)
            .guardWhile(() -> selectionChangingOperation());
    

Note: In some cases, you can provide consistency guaranties just by arranging the order of invalidation propagation among dependent properties and/or bindings. This solution, however, relies on the implementation detail that property and binding implementations provided by JavaFX itself notify the listeners in the order they were registered. It is therefore fragile to future changes or alternative property/binding implementations. Also, it gets complicated very quickly.

What about observable collections?

InhiBeans currently provide a wrapper for ObservableList that is able to temporarily block the notifications of list changes. All changes made to the list while blocked are combined into a single list change that is fired upon release. Blockable wrappers for other collections might be added in the future.

comments powered by Disqus