RFR: 8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue [v5]

Nir Lisker nlisker at openjdk.java.net
Sun Jan 16 22:04:39 UTC 2022


On Mon, 10 Jan 2022 21:09:15 GMT, John Hendrikx <jhendrikx at openjdk.org> wrote:

>> This is an implementation of the proposal in https://bugs.openjdk.java.net/browse/JDK-8274771 that me and Nir Lisker (@nlisker) have been working on.  It's a complete implementation including good test coverage.  
>> 
>> This was based on https://github.com/openjdk/jfx/pull/434 but with a smaller API footprint.  Compared to the PoC this is lacking public API for subscriptions, and does not include `orElseGet` or the `conditionOn` conditional mapping.
>> 
>> **Flexible Mappings**
>> Map the contents of a property any way you like with `map`, or map nested properties with `flatMap`.
>> 
>> **Lazy**
>> The bindings created are lazy, which means they are always _invalid_ when not themselves observed. This allows for easier garbage collection (once the last observer is removed, a chain of bindings will stop observing their parents) and less listener management when dealing with nested properties.  Furthermore, this allows inclusion of such bindings in classes such as `Node` without listeners being created when the binding itself is not used (this would allow for the inclusion of a `treeShowingProperty` in `Node` without creating excessive listeners, see this fix I did in an earlier PR: https://github.com/openjdk/jfx/pull/185)
>> 
>> **Null Safe**
>> The `map` and `flatMap` methods are skipped, similar to `java.util.Optional` when the value they would be mapping is `null`. This makes mapping nested properties with `flatMap` trivial as the `null` case does not need to be taken into account in a chain like this: `node.sceneProperty().flatMap(Scene::windowProperty).flatMap(Window::showingProperty)`.  Instead a default can be provided with `orElse`.
>> 
>> Some examples:
>> 
>>     void mapProperty() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(Bindings.createStringBinding(() -> text.getValueSafe().toUpperCase(), text));
>> 
>>       // Fluent: much more compact, no need to handle null
>>       label.textProperty().bind(text.map(String::toUpperCase));
>>     }
>> 
>>     void calculateCharactersLeft() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(text.length().negate().add(100).asString().concat(" characters left"));
>> 
>>       // Fluent: slightly more compact and more clear (no negate needed)
>>       label.textProperty().bind(text.orElse("").map(v -> 100 - v.length() + " characters left"));
>>     }
>> 
>>     void mapNestedValue() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(Bindings.createStringBinding(
>>         () -> employee.get() == null ? ""
>>             : employee.get().getCompany() == null ? ""
>>             : employee.get().getCompany().getName(),
>>         employee
>>       ));
>> 
>>       // Fluent: no need to handle nulls everywhere
>>       label.textProperty().bind(
>>         employee.map(Employee::getCompany)
>>                 .map(Company::getName)
>>                 .orElse("")
>>       );
>>     }
>> 
>>     void mapNestedProperty() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(
>>         Bindings.when(Bindings.selectBoolean(label.sceneProperty(), "window", "showing"))
>>           .then("Visible")
>>           .otherwise("Not Visible")
>>       );
>> 
>>       // Fluent: type safe
>>       label.textProperty().bind(label.sceneProperty()
>>         .flatMap(Scene::windowProperty)
>>         .flatMap(Window::showingProperty)
>>         .orElse(false)
>>         .map(showing -> showing ? "Visible" : "Not Visible")
>>       );
>>     }
>> 
>> Note that this is based on ideas in ReactFX and my own experiments in https://github.com/hjohn/hs.jfx.eventstream.  I've come to the conclusion that this is much better directly integrated into JavaFX, and I'm hoping this proof of concept will be able to move such an effort forward.
>
> John Hendrikx has updated the pull request incrementally with one additional commit since the last revision:
> 
>   Fix grammar mistakes and did some small rephrases

The API looks good to me. After you get the other reviewers to look at it you can update the CSR.

As for the fluent binding tests:
* The `private` fields in `ObservableValueFluentBindingsTest` can be `final`.
* Most tests that I have seen in JavaFX use the assert overloads that include a message that explains what the value should be or what it means if the assertion failed. I don't know how much of a requirement it is. I can help write these if you want.
* There is a utility class `JMemoryBuddy` that was added to JavaFX to test for memory leaks. This would have been helpful in the GC tests, but it seems that the class is not suited too well for this case (it requires you to `null` you own reference etc.). I think the way you did it is fine, but maybe that class should be updated (in a different patch) to be more welcoming for these checks.

I would also add tests of chaining the observables in common use cases. I wrote some myself to test it  for `map`:

ObservableValue<String> observableValue1 = property.map(v -> v + "Z");
ObservableValue<String> observableValue2 = observableValue1.map(v -> v + "X");
            
assertEquals("AZ", observableValue1.getValue());
assertEquals("AZX", observableValue2.getValue());
            
property.set("B");
            
assertEquals("BZ", observableValue1.getValue());
assertEquals("BZX", observableValue2.getValue());


for `orElse`:


ObservableValue<String> observableValue1 = property.map(v -> v + "Z");
ObservableValue<String> observableValue2 = observableValue1.orElse("K");
            
assertEquals("AZ", observableValue1.getValue());
assertEquals("AZ", observableValue2.getValue());
            
property.set("B");
            
assertEquals("BZ", observableValue1.getValue());
assertEquals("BZ", observableValue2.getValue());

property.set(null);
            
assertNull(observableValue1.getValue());
assertEquals("K", observableValue2.getValue());


You can clean these up and use them or write your own. `flatMap` should also have one. I can clean mine up and post it if it helps (I did some dirty testing there).

modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java line 189:

> 187:      *     holds {@code null}; can be {@code null}
> 188:      * @return an {@code ObservableValue} holding this {@code ObservableValue}'s value,
> 189:      *     or the given value it is {@code null}; never returns {@code null}

`or the given value it is {@code null}` missing "when" or "if"?

modules/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java line 271:

> 269:                 @Test
> 270:                 void shouldReturnPropertyValuesWithOperationApplied() {
> 271:                     assertEquals((Integer) 65, observableValue.getValue());

I don't think that the cast is needed as autoboxing will take care of it. Fine to leave as-is.

-------------

PR: https://git.openjdk.java.net/jfx/pull/675


More information about the openjfx-dev mailing list