1

What is the cleanest way to obtain progress from a class that will also be used in a no-gui environment for the purpose of displaying said status in a JavaFX Gui?

Example:

import java.util.ArrayList;
import java.util.List;

public class ExampleFetcher {
    public List<Entity> fetch() {
        final List<Entity> results = new ArrayList<>();

        for (int i = 0; i < N; i++) {
            final Entity current = //Do some heavy work

            results.add(current);
        }

        return results;
    }
}

There are several possibilities, but to me each of them has some drawbacks

1. "The JavaFX way": Use an observable property from javafx.beans

import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;

import java.util.ArrayList;
import java.util.List;

public class ExampleFetcher {
    private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper();

    public List<Entity> fetch() {

        final List<Entity> results = new ArrayList<>();

        for (int i = 0; i < N; i++) {
            final Entity current = //Do some heavy work

            progress.set((double) i / N);

            results.add(current);
        }

        return results;
    }

    public double getProgress() {
        return progress.get();
    }

    public ReadOnlyDoubleProperty progressProperty() {
        return progress.getReadOnlyProperty();
    }
}

Now you can simply observe the progressProperty or add a listener, which is quite nice, but it clutters the API a little. Also, is it even ok for a class to have a dependency on javafx if it's not going to be used in some cases?

2. The naive way: Add consumers to the called method

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class ExampleFetcher {
    public List<Entity> fetch(final Consumer<Double> progressConsumer) {
        final List<Entity> results = new ArrayList<>();

        for (int i = 0; i < N; i++) {
            final Entity current = //Do some heavy work

            progressConsumer.accept((double) i / N);

            results.add(current);
        }

        return results;
    }
}

You can pass a consumer that will update your progress bar or whatever you're using in the GUI. This avoids the dependency on javafx but adds a lot of clutter to the API, especially if you're not only monitoring progress but some other kind of status.


Now, is there another way I'm not thinking about? Is one of the ways provided actually decent? Or should I maybe look into another way of executing my tasks?

0

2 Answers 2

1

Depending on your goals and purpose here, I would strongly encourage solution #2 for the majority of scenarios - however, there is another option not mentioned that I'm going to bring up just for completeness sake:

You can present a standard property, and expect people to poll it at whatever period is most amenable to them as the consumer. This is not ideal, and not particularly pleasant, but in certain scenarios where you want to have guaranteed update times for your progress (if you have multiple things happening and want to update progresses between their activities for instance), you may go this route.

import java.util.ArrayList;
import java.util.List;

public class ExampleFetcher implements Runnable {
    private progress double = 0.0;
    private List<Entity> results = new ArrayList<>();

    public void run() {
        for (int i = 0; i < N; i++) {
            final Entity current = //Do some heavy work

            progress = ((double) i / N);

            results.add(current);
        }
    }

    public double getProgress() {
        return progress;
    }

    public List<Entity> getResults() {
        return results;
    }
}

public class SomeApp {

    public void doStuff() {

        final ExampleFetcher fetcher1 = new ExampleFetcher();
        final ExampleFetcher fetcher2 = new ExampleFetcher();
        final Thread f1Thread = new Thread(fetcher1);
        final Thread f2Thread = new Thread(fetcher2);
        f1Thread.start();
        f2Thread.start();

        while(!f1Thread.isAlive || !f2Thread.isAlive) {
            Thread.sleep(500);
        }

        while(f1Thread.isAlive && f2Thread.isAlive) {
            final double f1Progress = fetcher1.getProgress();
            // do something with progress! Or.. whatever you want?
            final double f2Progress = fetcher2.getProgress();
            // do something with progress! Or.. whatever you want?
            Thread.sleep(500);
        }
    }
}

Only real benefit here is - when you are firing callbacks inbetween your activity in the solution #2 you described above, you'll be utilizing the worker thread to do updates as that's what thread executes the callback (this may mean UI thread actions, which can get problematic). So each callback would have to do some cross thread communication to not block your fetchers work while it's doing progress updates, or if it needs to send updates to a UI thread, making your callbacks a little stranger.

Frankly, this is totally resolvable by writing your callbacks with simple patterns that make them non blocking and using standard thread communication. I would still personally go with solution #2.

My solution of polling described here though is occasionally preferable. Like I said above, if you wanted to control when the updates happen rather than letting them happen whenever the fetcher has an update, also by threading them off like this you can have multiple running at once rather than if they're running on the same thread using callbacks (even with the callback approach, sharing a thread is ill advised), they'll need to run in sequence since they may need to share your front-end thread. The polling approach also allows a built in monitoring which will let you put timeouts on how long between progress updates is allowed. If you just expect callbacks to notify you when progress is made, and the fetcher locks or dies before completing, it's going to require some kind of monitor anyway to guarantee you know "it finished! Unsuccessfully after waiting for 5 minutes."

Food for thought! Lean on solution #2 unless you have a very specific reason to prefer polling!

2

If in doubt, use the most general solution possible. Here, that's using a callback, i.e. the Consumer (solution #2). You can also think of this as a ProgressWatchingStrategy if that's helpful.

If desired by a user of this class, the JavaFX property can then be trivially implemented on top of this, e.g.:

ReadOnlyDoubleWrapper progressProperty = new ReadOnlyDoubleWrapper();
... // set up listeners
fetcher.fetch(progressProperty::set);

While the property-based solution (#1) is effectively equivalent (you could also implement the callback-based solution on top of it), it introduces extra concepts and requires more code for basically no gain. The only reason to prefer the property field would be if the fetcher object were used multiple times, and the same listeners would be interested in the progress of multiple fetch() calls on that same object.

4
  • ...and requires more code for basically no gain. -- Well, it eliminates the tightly-bound coupling to JavaFX which, depending on your goals, could be a really big deal. Commented Feb 5, 2018 at 19:40
  • @Robert Misunderstanding: the “more code for no gain” is solution #1 which has the JavaFX dependency. I've edited the answer to clarify. Sorry :D
    – amon
    Commented Feb 5, 2018 at 19:44
  • Most general solutions violate YAGNI. In this particular case, this works, but in general, this advice is VERY dangerous.
    – Basilevs
    Commented Feb 6, 2018 at 19:50
  • @Basilevs You're right that features shouldn't be added before there's a need for them. But we also shouldn't constrain the code unnecessarily (except of course in security-sensitive contexts). Here, using a callback doesn't add unnecessary features. Instead, it simplifies the code by taking away the coupling to JavaFX. So I think, this kind of generality is YAGNI-compliant.
    – amon
    Commented Feb 6, 2018 at 19:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.