Вопрос или проблема
У меня есть приложение 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
из любых мест (в том числе из каких-либо тестовых случаев), обеспечивая при этом полный контроль над прогрессом выполнения и обработкой завершённых результатов.