繁体   English   中英

什么数据结构适合事件和监听器的顺序事件调度系统?

[英]What data structure would fit a sequential event dispatch system of events and listeners?

我需要为以下情况找到合适的数据结构。 我编写了一个包含事件和监听器的简单事件调度系统。 系统是完全顺序的 ,因此没有任何并发​​和同步问题。

要求和想法

  • 每个侦听器向预定义(编译时)注册一种或多种类型的事件。
  • 监听器可以在运行时注册和注销。
  • 必须维护侦听器注册的顺序,因为它是接收事件的顺序(侦听器总是在末尾添加,但可以从任何地方删除)。
  • 事件类型可以随时注册0个或更多侦听器以接收它。

这种关系的可视化可以解释为表格:

       | Listener1 | Listener2 | Listener3 | Listner5
-----------------------------------------------------
Event1 |     X     |           |     X     |    X
-----------------------------------------------------
Event2 |           |     X     |     X     |    X
-----------------------------------------------------
Event3 |           |           |           |
-----------------------------------------------------
Event4 |           |           |           |    X
-----------------------------------------------------
  • 行的顺序无关紧要。
  • 必须保持列的顺序。
  • 可以添加和删除列。
  • 行数在运行时是常量。

我的想法是:

  • 调度事件时,我需要知道注册哪些侦听器才能接收事件类型。 我正在考虑一个Event -> Collection<Listener>映射,其中键是无序的。 侦听器集合需要维护插入顺序(或者,如果可以使用以下点,则不需要)。
  • 我需要保留一组维护插入顺序的侦听器,而不管它们注册的事件(此要求来自侦听器附加到的对象)。 因为即使上面的Collection<Listener>是按插入顺序排列的,它也是每个事件类型而不是全局的。 我正在考虑OrderedCollection<Listener>
  • 取消注册时,我需要找到一个监听器已注册的所有事件类型,以便可以将其删除。 我正在考虑一个Listener -> Collection<Event> ,除非第一点的映射可以对值位置的列表执行removeAll(Listener)操作,但是这可以留下空列表而不是完全删除键。

在我看来,双向多图适合这种情况。 支持多重映射是OrderedMap<Listener, UnorderedCollection<Event>> UnorderedMap<Event, OrderedCollection<Listener>>OrderedMap<Listener, UnorderedCollection<Event>> 如果可以使用OrderedMap的排序,则可能不需要订购OrderedCollection<Listener> 显然,可以订购任何无序数据结构,但这是不必要的。

或者,我已经看到了具有reverse / invert操作的单个多图的想法,以获得相反的映射。 我关心的是双重的:

  1. 我不知道一方是否可以订购其钥匙而另一方则没有。
  2. 这似乎是一个昂贵的操作,如果我需要一直反转映射它可能是低效的。

伪代码用法

注册一个监听器:

// somewhere
Listener1 listener = new Listener1();
listener.register(); 

// class definition
class Listener1 extends AbstractListener {

    void register() {

        DataStructure.put(Event1.class, this);
        DataStructure.put(Event2.class, this);

        // or
                                 // some collection
        DataStructure.put(this, [Event1.class, Event2.class])
    }
}

派遣活动:

// somewhere
EventManager.dispatch(new Event2());

// class definition
class EventManager {

    static void dispatch(Event event) {

        OrderedCollection<Listener> listeners = DataStructure.get(event);
        listeners.forEach(l -> l.processEvent(event)); // forEach maintains the order
    }
}

取消注册一个监听器:

Listener2 listener = ...;        // got it from somewhere
DataStructure.remove(listener);  // remove from everywhere and if a key is left with an empty list remove that key

最后的细节和问题

如果重要的话,大约有30种事件类型,并发注册监听器的数量是O(10),尽管在程序的生命周期内可能有O(100)个监听器 - 其中大多数是取消注册和GC。

关于订购的说明。 由于侦听器具有增量唯一int id字段,因此我可以确保按id号排序等同于按注册(插入)顺序排序。 目前的差异是id在侦听器的初始化时设置,而注册在之后完成(并且可以在该间隙中创建和注册另一个侦听器)。 如果在这种情况下使用有序集合的排序集合有一个优势,我可以做一些工作来保证按id排序等同于插入顺序。

哪些数据结构符合我的需求? 我是否需要自己编写(我宁愿不重新发明轮子)还是已经实现了(番石榴,我正在看着你)?

如果我很好理解你的问题,那么最好的结构似乎是一个AbstractListener列表数组。

List<AbstractListener>[]

这里按要求描述要求:

要求:监听器可以在运行时注册和取消注册。

List可以处理添加删除而不会出现问题

要求:必须维护监听器注册的顺序,因为它是接收事件的顺序(监听器总是在最后添加,但可以从任何地方删除)。

列表保留了订单。 来自javadoc

有序集合(也称为序列)

要求:事件类型可以随时注册0个或更多侦听器以接收它。

List可以包含0个或更多元素。

要求:必须保持列的顺序。

列表保留元素的顺序。

要求:可以添加和删除列。

List可以添加或删除元素(列)

要求:行的顺序无关紧要。

任何数据结构都没问题

要求:行数在运行时期间是常量。

数组具有恒定的大小


从您的评论到答案似乎另一种可能的解决方案是定义一个包含所有事件的类:

public interface Listener {
    public void handleEvent(Event event);
}

public class EventGenerator {
    private List<Listener> listeners;

    public void addListener(Listener listener) {
        listeners.add(listener);
    }

    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    public void doSomething(Event event) {
        for (Listener listener : listeners) {
            listener.handleEvent(event);
        }
    }
}

对于每种事件,你可以做类似的事情:

// Create an event generator for each kind of event
EventGenerator eventGenerator1 = new EventGenerator();

EventGenerator eventGenerator2 = new EventGenerator();

// Register listeners to eventGenerators
eventGenerator1.addListener(listener1);
eventGenerator1.addListener(listener2);
eventGenerator1.addListener(listener3);

eventGenerator2.addListener(listener1);
eventGenerator2.addListener(listener2);
eventGenerator2.addListener(listener3);

...

// Eventually remove a listener
eventGenerator2.removeListener(listener2);

...

// When doSomething is invoked all listeners on that eventGenerator are invoked
eventGenerator1.doSomething(event1);  // Dispatched to listener 1, 2 and 3
eventGenerator1.doSomething(event2);  // Dispatched to listener 1, 2 and 3
eventGenerator1.doSomething(event3);  // Dispatched to listener 1, 2 and 3
eventGenerator1.doSomething(event4);  // Dispatched to listener 1, 2 and 3

eventGenerator1.removeListener(listener3);

eventGenerator1.doSomething(event5);  // Dispatched to listener 1, 2

eventGenerator1.removeListener(listener1);

eventGenerator1.doSomething(event6);  // Dispatched to listener 2

由于监听器可以附加到许多事件,我会尝试通过使EventListener之间的关系变得独特来“线性化”问题:

public final class Relation {
    private final Class<? extends Event> eventType;
    private final Listener listener;

    public Relation(Class<? extends Event> eventType, Listener listener) {
        this.eventType = eventType;
        this.listener = listener;
    }

    public Class<? extends Event> getEventType() {
        return eventType;
    }

    public Listener getListener() {
        return listener;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Relation) {
            Relation that = (Relation) obj;

            return this.eventType.equals(that.eventType) && this.listener.equals(that.listener);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(eventType, listener);
    }
}

EventManager应该封装用于存储这些关系的整个数据结构

public final class EventManager {

    private Set<Relation> relations = new LinkedHashSet<>();

    public void dispatch(Event e)
    {
        relations.stream()
                 .filter(r -> r.getEventType().isInstance(e))
                 .forEach(r -> r.getListener().processEvent(e));
    }

    public void register(Class<? extends Event> e, Listener l)
    {
        relations.add(new Relation(e, l));
    }

    public void unregister(Listener l)
    {
        relations = relations.stream()
                             .filter(r -> !r.getListener().equals(l))
                             .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    public static void main(String[] args) {
        Event e1 = new Event1();
        Event e2 = new Event2();
        Event e4 = new Event4();
        Listener l1 = new Listener1();
        Listener l2 = new Listener2();
        Listener l3 = new Listener3();
        Listener l5 = new Listener5();
        EventManager em = new EventManager();

        em.register(Event1.class, l1);
        em.register(Event1.class, l3);
        em.register(Event1.class, l5);
        em.register(Event2.class, l2);
        em.register(Event2.class, l3);
        em.register(Event2.class, l5);
        em.register(Event4.class, l5);

        System.out.println("After register");
        em.dispatch(e1); //prints hello from 1, 3, 5
        em.dispatch(e2); //prints hello from 2, 3, 5
        em.dispatch(e4); //prints hello from 5

        em.unregister(l3);

        System.out.println("After unregistering l3");
        em.dispatch(e1); //prints hello from 1, 5
        em.dispatch(e2); //prints hello from 2, 5
        em.dispatch(e4); //prints hello from 5

        em.unregister(l5);

        System.out.println("After unregistering l5");
        em.dispatch(e1); //prints hello from 1
        em.dispatch(e2); //prints hello from 2
        em.dispatch(e4); //prints nothing
    }
}

我已经看了几个选项,并且通过一些基本的理论考虑,我找到了解决方案。

Map<Event, Collection<Listener>> // Event stands for Class<? extends Event>

其中每个键的键和侦听器都是有序的。

理论上的考虑

给定Map<Event, Collection<Listener>>的映射(多图),如果键和值集合都是插入排序的,则可以获得Map<Event, Collection<Listener>>的(全局)插入顺序。 我不会在这里给出一个证明,只是一个简短的解释。 此数据结构等效于有序集合的有序集合。 执行展平操作将导致单个有序集合与重复。 然后通过消除后续出现来删除重复项将导致按插入顺序排序的集合。

有趣的是,当映射和值集合中的任何一个或两者都不维护插入顺序时,如何不维护全局侦听器插入顺序。

抛开理论,这样的数据结构:

  • 在插入和删除操作期间,保持全局和每个键的侦听器的插入顺序。
  • 借助地图,可以轻松调度Event -> <Listener>
  • 不允许轻松注销Listener -> <Event> (非双向)。
  • 保持某种事件排序,这是无关紧要的。

伪代码用法

注册,发送和注销:

class EventDispatcher {

    static Map<Event, Collection<Listener>> map;

    static void registerListener(Collection<Event> eventTypes, Listener listener) {

        eventTypes.forEach(et -> map.put(et, listener));
    }

    static void dispatch(SomeEvent event) {

        map.get(SomeEvent.getClass()).forEach(l -> l.trigger(event));
    }

    static void deregisterListener(Listener listener) {

        while (map.values().remove(listener));
    }
}

获取全局插入顺序的侦听器是通过以下方式完成的:

new LinkedHashSet<>(map.values());

因为map.values()返回了一个展平的集合,而Set折叠了重复的集合。

如上所述,调度是有效的,因为只有注册接收事件的听众才会收到它。 注销具有不知道从哪个键移除指定的侦听器的问题,因此需要对所有键进行迭代,这是无效的。 可以反转映射,但我相信它不会更快。

相反,维护逆映射,

Map<Listener, Collection<Event>>

是优选的

  • 与事件调度相比,收听者撤销/注册的频率很高。
  • 与(并发)已注册侦听器的数量相比,事件类型的数量很多。
  • 与事件类型的数量相比,侦听器注册的事件类型的数量较少。

实现

Java Collections框架提供了几个维护插入顺序的实现。 对于地图,只有LinkedHashMap可行。 对于值集合, ArrayListLinkedListLinkedHashSet是可行的。

上述情况,番石榴提供多重映射实现LinkedHashMapLinkedListLinkedHashSet 后者保证没有重复的值。 由于我的监听器在设计上是独特的(如上所述,它们具有用于equals的唯一int id ),因此不需要显式保证。 它们之间也存在迭代顺序差异,但我相信只有当侦听器可以部分取消注册(来自特定事件类型)时才会显示,这不是我的情况。

对于我正在处理的O(10-100)数量级,几乎没有考虑性能差异,我还没有选择具体的实现。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM