JavaFX.concurrent.Task: Как отделить библиотеку от GUI?

Вопрос или проблема

У меня есть приложение JavaFX, в котором одна из функций заключается в поиске (больших) бинарных данных по конкретным шаблонам. Чтобы интерфейс оставался отзывчивым, я создаю параллельную задачу и помещаю её в отдельный поток:

public void search(SearchPattern pattern) {
    
    Task<ObservableList<...>> task = new Task<>() {
        @Override protected ObservableList<...> call() throws Exception {

            try {                    
                for(int i=0;i<....size();i++) {
                    if (isCancelled()) {
                        break;
                    }
                    if(i%50000 == 0) {
                        updateProgress(i, entries.size());
                    }
                    if(pattern.matches(entries.get(i))) {
                        // выполнить действия
                    }                        
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // очистка ввода/вывода
            }
            return FXCollections.observableArrayList(...);
        }
    };

    progressBar.progressProperty().bind(task.progressProperty());

    task.setOnSucceeded(e -> {
        unregisterRunningTask(task);
        searchResults = task.getValue();
    });

    Thread thread = new Thread(task);
    registerRunningTask(task);
    thread.setDaemon(false);
    thread.start();
}

Это работает нормально, однако смешивает (библиотечные) функции с кодом GUI. В частности, я хотел бы вынести код поиска в отдельную библиотеку, написать тестовые случаи для этой функции – полностью автоматизированные, без GUI – и, конечно, при тестировании из командной строки отдельный поток не нужен. Сейчас я ограничен тем, что должен помещать pattern.matches(entries.get(i)) в ту библиотеку. Более чистый способ заключался бы в том, чтобы иметь функцию

searchpattern(filename) {
    for(int=0;i< ... ;i++) {
       ...
    }
    return observableArrayList(...);
}

в библиотеке, которая включает полный цикл, написать тестовые случаи и бенчмарки для этого, а затем каким-то образом включить эту функцию в код GUI. Проблема, с которой я сталкиваюсь при разделении этого кода, заключается в следующем:

updateProgress() требует ссылки на задачу. Конечно, я всегда мог бы создать задачу, пытаясь реализовать тестовые случаи, т.е. даже при вызове searchpattern(filename) из контекста, не связанного с GUI. Я мог бы просто опустить любые обновления прогресса в своем коде, но это создало бы очень плохой пользовательский опыт.

Существует ли другой способ передать обновления из этой (потенциально) продолжительной функции, используя какой-то абстрактный слой, который сделает эту функцию удобной в библиотеке как для (поточного) контекста GUI, так и для контекста командной строки без потоков?

Ответ или решение

Для того чтобы отделить функциональность поиска от пользовательского интерфейса и сделать ваш код более чистым, удобным для тестирования и повторного использования, можно воспользоваться шаблоном проектирования, известным как "Наблюдатель" (Observer pattern). Этот подход позволит вам уведомлять GUI о прогрессе выполнения задачи, не смешивая библиотечный код с кодом графического интерфейса.

Ниже приведён пример, как можно реализовать это:

1. Определите интерфейс для уведомлений о прогрессе

Создайте интерфейс, который будет использоваться для уведомлений о прогрессе:

public interface ProgressListener {
    void onProgressUpdate(int current, int total);
    void onCompletion(ObservableList<?> results);
    void onError(Exception e);
}

2. Реализуйте библиотечный класс для поиска

На следующем шаге создайте класс, который будет реализовывать логику поиска. Этот класс будет принимать объект ProgressListener и использовать его для уведомлений о прогрессе:

public class SearchLibrary {

    public void searchPatterns(SearchPattern pattern, List<Entry> entries, ProgressListener listener) {
        new Thread(() -> {
            try {
                for (int i = 0; i < entries.size(); i++) {
                    if (shouldStop()) {
                        return; // Логика остановки выполнения, если она потребуется
                    }
                    if (i % 50000 == 0) {
                        listener.onProgressUpdate(i, entries.size());
                    }
                    if (pattern.matches(entries.get(i))) {
                        // Добавьте соответствующий элемент в результаты поиска
                        // Например, добавьте его в список результатов
                    }
                }

                // по завершении дальнейшая логика
                listener.onCompletion(FXCollections.observableArrayList(...));
            } catch (Exception e) {
                listener.onError(e);
            } finally {
                // Очистка ресурсов после выполнения
            }
        }).start();
    }

    private boolean shouldStop() {
        // Логика проверки, следует ли остановить процесс
        return false; // заглушка
    }
}

3. Интегрируйте это в ваш GUI

Теперь в вашем классе GUI просто создайте экземпляр SearchLibrary и передайте обработчик для обновления прогресса:

public void search(SearchPattern pattern) {
    SearchLibrary searchLibrary = new SearchLibrary();

    ProgressListener listener = new ProgressListener() {
        @Override
        public void onProgressUpdate(int current, int total) {
            // Обновляем прогресс-бар
            progressBar.setProgress((double) current / total);
        }

        @Override
        public void onCompletion(ObservableList<?> results) {
            // Обработка результатов поиска
            searchResults = results;
        }

        @Override
        public void onError(Exception e) {
            e.printStackTrace(); // Логирование ошибки
        }
    };

    searchLibrary.searchPatterns(pattern, entries, listener);
}

4. Тестирование

Теперь, когда ваша логика поиска отделена от GUI, вы можете тестировать класс SearchLibrary отдельно от графического интерфейса с помощью библиотеки тестирования JUnit. Вы можете создать фэйковые имплементации ProgressListener, если это необходимо, для проверки требований к прогрессу и завершению выполнения.

Заключение

Такой подход не только позволяет нам отделить бизнес-логику от пользовательского интерфейса, но и упрощает тестирование. Вы можете вызывать метод searchPatterns из любых мест (в том числе из каких-либо тестовых случаев), обеспечивая при этом полный контроль над прогрессом выполнения и обработкой завершённых результатов.

Оцените материал
Добавить комментарий

Капча загружается...