Статьи

Не удаляйте слушателей — используйте ListenerHandles

Слушать наблюдаемый экземпляр и реагировать на его изменения — это весело. Делать то, что необходимо, чтобы прервать или закончить прослушивание, — это гораздо менее весело. Давайте посмотрим, откуда возникла проблема и что с этим можно сделать.

обзор

Пост сначала опишет ситуацию, прежде чем обсуждать общий подход и что с ним не так. Затем будет представлена ​​простая абстракция, которая решает большинство проблем.

Хотя в примерах используется Java, недостаток присутствует и во многих других языках. Предлагаемое решение может быть применено на всех объектно-ориентированных языках. Те, кому лень внедрять абстракцию в самих Java, могут использовать LibFX .

Ситуация

Скажем, мы хотим слушать изменения значения свойства. Это прямо вперед:

Простой случай, который не поддерживает удаление

1
2
3
private void startListeningToNameChanges(Property<String> name) {
    name.addListener((obs, oldValue, newValue) -> nameChanged(newValue));
}

Теперь предположим, что мы хотим прервать прослушивание во время определенных интервалов или полностью остановиться.

Хранение ссылок вокруг

Наиболее распространенный подход для решения этой проблемы — сохранить ссылку на слушателя, а другую — на свойство вокруг. В зависимости от конкретного варианта использования реализации могут отличаться, но все они сводятся к следующему:

Удаление слушателя по умолчанию

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private Property<String> listenedName;
private ChangeListener<String> nameListener;
 
...
 
private void startListeningToNameChanges(Property<String> name) {
    listenedName = name;
    nameListener = (obs, oldValue, newValue) -> nameChanged(newValue);
    listenedName.addListener(nameListener);
}
 
private void stopListeningToNameChanges() {
    listenedName.removeListener(nameListener);
}

Хотя это может выглядеть нормально, я убежден, что на самом деле это плохое решение (хотя и по умолчанию).

Во-первых, дополнительные ссылки загромождают код. Трудно заставить их выразить намерение, почему они хранятся рядом, поэтому они снижают читабельность.

Во-вторых, они увеличивают сложность, добавляя новый инвариант к классу: свойство всегда должно быть тем, к которому был добавлен слушатель. В противном случае вызов removeListener ничего не сделает, а слушатель все равно будет выполнен при будущих изменениях. Разрешить это может быть неприятно. Хотя поддерживать этот инвариант легко, если класс короткий, он может стать проблемой, если он станет более сложным.

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

Эти аспекты уже лишают этого права быть решением по умолчанию. Но это еще не все! Необходимость делать это во многих классах приводит к дублированию кода. И, наконец, реализация выше содержит условие гонки.

ListenerHandle

Большинство проблем связано с обработкой наблюдаемого и слушателя непосредственно в классе, который должен прервать / завершить прослушивание. В этом нет необходимости, и все эти проблемы исчезают благодаря простой абстракции: ListenerHandle .

ListenerHandle

1
2
3
4
public interface ListenerHandle {
    void attach();
    void detach();
}

ListenerHandle придерживается ссылок на наблюдаемое и слушателя. При вызовах attach() или detach() он либо добавляет слушателя к наблюдаемой, либо удаляет его. Для того чтобы это было встроено в язык, все методы, которые в настоящее время добавляют слушателей в наблюдаемые, должны возвращать дескриптор этой комбинации.

Теперь осталось только реализовать дескрипторы для всех возможных сценариев. Или убедите тех, кто разрабатывает ваш любимый язык программирования, сделать это. Это оставлено в качестве упражнения для читателя.

Обратите внимание, что это решает все проблемы, описанные выше, за исключением состояния гонки. Есть два способа решения этой проблемы:

  • реализации дескриптора могут быть поточно-ориентированными
  • может быть реализован синхронизирующий декоратор

ListenerHandles в LibFX

В качестве разработчика Java вы можете использовать LibFX , который поддерживает дескрипторы слушателей на трех уровнях.

Особенности знают о ListenerHandles

Каждая функция LibFX, которая может делать это без конфликта с Java API, возвращает ListenerHandle при добавлении слушателей.

Возьмем WebViewHyperlinkListener в качестве примера:

Получение ‘ListenerHandle’ для ‘WebViewHyperlinkListener’

1
2
3
4
WebView webView;
 
ListenerHandle eventProcessingListener = WebViews
    .addHyperlinkListener(webView, this::processEvent);

Утилиты для JavaFX

Поскольку LibFX имеет сильные связи с JavaFX (кто бы мог подумать!), Он предоставляет служебный класс, который добавляет прослушиватели к наблюдаемым и возвращает дескрипторы. Это реализовано для всех наблюдаемых / слушающих комбинаций, которые существуют в JavaFX.

В качестве примера давайте рассмотрим комбинацию ObservableValue<T> / ChangeListener<? superT> ChangeListener<? superT> :

Некоторые методы в «ListenerHandles»

1
2
3
4
5
6
7
public static <T> ListenerHandle createAttached(
        ObservableValue<T> observableValue,
        ChangeListener<? super T> changeListener);
 
public static <T> ListenerHandle createDetached(
        ObservableValue<T> observableValue,
        ChangeListener<? super T> changeListener);

ListenerHandleBuilder

Во всех других случаях, т.е. для любой комбинации наблюдаемый / слушатель, не описанной выше, дескриптор может быть создан с помощью компоновщика:

Создание «ListenerHandle» для пользовательских классов

01
02
03
04
05
06
07
08
09
10
11
// These classes do not need to implement any special interfaces.
// Their only connection are the methods 'doTheAdding' and 'doTheRemoving',
// which the builder does not need to know about.
MyCustomObservable customObservable;
MyCustomListener customListener;
 
ListenerHandles
        .createFor(customObservable, customListener)
        .onAttach((obs, listener) -> obs.doTheAdding(listener))
        .onDetach((obs, listener) -> obs.doTheRemoving(listener))
        .buildAttached();

Реактивное программирование

Хотя это не пост о реактивном программировании , его все же следует упомянуть. Проверьте ReactiveX (для многих языков, включая Java, Scala, Python, C ++, C # и другие) или ReactFX (или этот вводный пост ) для некоторых реализаций.

отражение

Мы видели, что подход по умолчанию для удаления слушателей из наблюдаемых создает ряд опасностей, и его следует избегать. Абстракция дескриптора слушателя обеспечивает чистый способ решения многих / всех проблем, а LibFX обеспечивает реализацию.

Ссылка: Не удаляйте слушателей — используйте ListenerHandles от нашего партнера JCG Николая Парлога в блоге CodeFx .