Статьи

EventBus: как публиковать и подписываться на платформе NetBeans


Таким образом, вы используете платформу NetBeans и знаете, что она позволяет создавать действительно модульную конструкцию, в которую можно подключать и отключать компоненты по вашему желанию. Но теперь, какой шаблон вы можете использовать, чтобы уменьшить связь между компонентами, которые должны взаимодействовать друг с другом? Рассмотрим, например, проблему, с которой я сталкиваюсь с
blueMarine : некоторые компоненты («исследователи») позволяют запрашивать внутреннюю базу данных изображений с другими критериями (например, просмотр файловой системы, по метаданным, по тегу …), а другие компоненты (‘ Представления ‘) показать результат по-разному (например, миниатюры или панели свойств или полный рендеринг) Какой подход лучше: чтобы исследователи зависели от зрителей, чтобы они могли напрямую вызывать метод для них? Или наоборот?

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

Действительно, это одна из первых частей, которую я разработал в blueMarine, когда я перенес ее на платформу NetBeans более двух лет назад, и это было довольно хорошее улучшение по сравнению с предыдущим проектом. Реализация была сфокусирована на классе Lookup платформы NetBeans, одном из самых мощных в платформе NetBeans. Хотя он в основном используется для поиска служб, зарегистрированных в приложении, он также используется средой выполнения платформы NetBeans для хранения соответствующих объектов (например, выбранных элементов в списке) и для уведомления слушателей (такие контекстно-зависимые действия). Поэтому я в основном использовал глобальный поиск, предоставляемый платформой NetBeans «Utilities.actionsGlobalContext ()», чтобы прослушивать выборки, сделанные в TopComponents. Это казалось довольно логичным способом работы,поскольку TopComponents обычно содержат ExplorerManager, который автоматически получает уведомления о выбранных узлах в деревьях или списках, размещенных внутри TopComponent. Итак, поток событий:

выберите узел в дереве -> уведомление опубликовано в ExplorerManager -> уведомление опубликовано для actionsGlobalContext () -> уведомление получено моим слушателем.

При таком подходе вы можете использовать информацию о классе в качестве «темы»: когда вы выбираете Node, все, что находится внутри его частного Lookup (например, DataObject), публикуется. DataObject, очевидно, является одной из наиболее часто используемых тем blueMarine (представляет фотографии) и предоставляет информацию о выборе одного объекта: для уведомления о том, что набор DataObjects был подготовлен, я определил определенный класс с именем DataObjectSelection; чтобы уведомить, что в фоновом режиме выполняется задача выбора, используется другой конкретный объект (DataObjectSelectionInProgress), чтобы зрители могли отобразить сообщение «Пожалуйста, подождите».

Идея была аккуратной, код реализации немного громоздким, и я столкнулся с некоторыми досадными ошибками. Основная проблема возникла из-за использования actionGlobalContext () и в зависимости от поведения TopComponent. Одна из вещей, которая иногда случалась, была эта последовательность: вы получаете файловый менеджер и рекурсивно выбираете каталог; в этот момент задание фонового сканирования начинается и длится некоторое время; тем временем вы активируете другой TopComponent (например, просмотрщик миниатюр, где вы ожидаете увидеть результаты в скором времени); на этом этапе сканер завершает работу и выбирает новые узлы в первом TopComponent, который, к сожалению, больше не является активным TopComponent. Итак, результаты не появляются. Чтобы их увидеть, нужно было снова активировать первый TopComponent. Слишком громоздко!

Несколько месяцев назад я узнал от Уэйда Чандлера , другого члена команды мечты NetBeans , что все можно сделать лучше. Он опубликовал очень компактный и аккуратный класс с именем CentralLookup : это одноэлементный объект, содержащий новый Lookup, который вы можете использовать по своему усмотрению , поскольку платформа NetBeans об этом не знает. Таким образом, вы можете сами выбирать свою политику публикации, не вмешиваясь в работу TopComponents. Иногда проблемы имеют аккуратные и простые решения.

Уэйд опубликовал свой код в инкубаторе PlatformX , и сегодня я использовал его в качестве основы для нового средства в blueMarine, названного EventBus. Основные функции предоставляются Wade’s CentralLookup, а EventBus оборачивает его более сфокусированным интерфейсом. По сути, мои исследователи теперь используют этот код для публикации:

DataObject myDataObject = ...
EventBus.getDefault().publish(myDataObject):

Зрители используют этот код для получения уведомлений:

    private final EventBusListener<DataObject> listener = new EventBusListener<DataObject>()
{
@Override
public void notify (final DataObject dataObject)
{
if (dataObject != null)
{
// do something
}
}
};

где слушатель зарегистрирован так:

EventBus.getDefault().subscribe(DataObject.class, listener);

Самая актуальная версия опубликована в OpenBlueSky, вы можете проверить ее с помощью Subversion по адресу https://openbluesky.dev.java.net/svn/openbluesky/trunk/src/OpenBlueSky/EventBus ), я также разместив код ниже:

import it.tidalwave.openide.eventbus.impl.ListenerAdapter;
import java.util.HashMap;
import java.util.Map;
import org.netbeans.platformx.centrallookup.api.CentralLookup;
import org.openide.util.Lookup.Result;

public class EventBus
{
private static final EventBus instance = new EventBus();

private final CentralLookup centralLookup = CentralLookup.getDefault();

private final Map<Class<?>, Result<?>> resultMapByClass = new HashMap<Class<?>, Result<?>>();

private final Map<EventBusListener<?>, ListenerAdapter<?>> adapterMapByListener = new HashMap<EventBusListener<?>, ListenerAdapter<?>>();

private EventBus()
{
}

public static EventBus getDefault()
{
return instance;
}

public void publish (final Object object)
{
if (object == null)
{
throw new IllegalArgumentException("object is mandatory");
}

for (final Object old : centralLookup.lookupAll(object.getClass()))
{
centralLookup.remove(old);
}

if (object != null)
{
centralLookup.add(object);
}
}

public void unpublish (final Class<?> topic)
{
for (final Object old : centralLookup.lookupAll(topic))
{
centralLookup.remove(old);
}
}

public synchronized <T> void subscribe (final Class<T> topic, final EventBusListener<T> listener)
{
Result<?> result = resultMapByClass.get(topic);

if (result == null)
{
result = centralLookup.lookupResult(topic);
resultMapByClass.put(topic, result);
result.allInstances();
}

final ListenerAdapter<T> adapter = new ListenerAdapter<T>(listener);
adapterMapByListener.put(listener, adapter);
result.addLookupListener(adapter);
}

public synchronized <T> void unsubscribe (final Class<T> topic, final EventBusListener<T> listener)
{
final Result<?> result = resultMapByClass.get(topic);

if (result == null)
{
throw new IllegalArgumentException(String.format("Never subscribed to %s", topic));
}

final ListenerAdapter<T> adapter = (ListenerAdapter<T>)adapterMapByListener.remove(listener);
result.removeLookupListener(adapter);
}
}

Это EventBusListener:

public interface EventBusListener<T>
{
public void notify (T object);
}

и, наконец, это класс реализации утилиты:

import it.tidalwave.openide.eventbus.EventBusListener;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;

public class ListenerAdapter<T> implements LookupListener
{
private final EventBusListener eventBusListener;

public ListenerAdapter (final EventBusListener eventBusListener)
{
this.eventBusListener = eventBusListener;
}

public void resultChanged (final LookupEvent event)
{
final Lookup.Result result = (Lookup.Result)event.getSource();

if (!result.allInstances().isEmpty())
{
eventBusListener.notify((T)result.allInstances().iterator().next());
}
else
{
eventBusListener.notify(null);
}
}
}