哪种数据结构适合顺序事件分发系统的事件和侦听器?

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

需求和想法

  • 每个监听器都注册到一个预定义(编译时)的1个或多个事件类型。
  • 监听器可以在运行时注册和注销。
  • 注册侦听器的顺序必须保持,因为它是它们接收事件的顺序(侦听器始终添加到末尾,但可以从任何位置删除)。
  • 事件类型可以有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)操作,但这可能会留下空列表,而不是完全删除键。

对我来说,双向多映射似乎适合这种情况。 支持多重映射的背景是 UnorderedMap<Event, OrderedCollection<Listener>>OrderedMap<Listener, UnorderedCollection<Event>>。 如果可以使用OrderedMap中的排序,则可能不需要对OrderedCollection<Listener>进行排序。 显然,任何无序数据结构都可以排序,但是没有必要。

或者,我看到单个多重映射的想法,其中包含一个反向/反转操作以获取相反的映射。 我的关注点有两个:

  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'd。

关于排序的说明。由于监听器具有增量唯一的int id字段,因此我可以通过id号码的排序来确保与注册(插入)顺序相同。目前的差异在于id是在初始化侦听器时设置的,而注册是之后完成的(在这段时间内可能会创建并注册另一个侦听器)。如果在这种情况下使用排序集合优于有序集合,则我可以付出一些努力来确保按id排序等同于按插入顺序排序。

哪种数据结构适合我的需求? 我需要编写自己的(我宁愿不要重复造轮子)还是已经实现了其中一个(Guava,我在看着你)?

3个回答

4
如果我理解您的问题正确的话,最好的结构似乎是一个AbstractListener列表的数组。
List<AbstractListener>[]

以下是要求的描述:

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

列表可以处理addremove,没有问题。

要求:注册侦听器的顺序必须保持不变,因为它是它们接收事件的顺序(侦听器始终添加到末尾,但可以从任何位置删除)。

列表维护了顺序。 来自javadoc

有序集合(也称为序列)

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

列表可以容纳0个或多个元素。

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

列表维护了元素的顺序。

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

一个列表可以添加或删除元素(列)。 要求:行的顺序无关紧要。
任何数据结构都没有问题。 要求:在运行时,行数保持不变。
数组具有恒定的大小。
从您对答案的评论中可以看出,另一个可能的解决方案是定义一个包含所有事件的类:
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

我对你的建议感到困惑。数组的大小是我拥有的事件类型数量,每个单元格都保存了注册到该事件的侦听器列表?如果是这样的话,(1)仍然缺少全局侦听器排序(“我的想法是”中的第二点)。 (2)您如何知道哪个事件类型对应于数组中的哪个索引?如果我需要分派SomeTypeEvent,我应该访问哪个单元格? - user1803551
关于您的编辑,当一个“监听器”被注销时,它将从所有事件监听职责中删除。这意味着每当我注销一个“监听器”,我都需要在所有“事件生成器”上调用“removeListener”。 - user1803551
如果您从EventGenerator中删除一个监听器,则所有后续事件都不会调用该监听器。这是指该类型的所有后续事件。那么其他类型的事件呢?如果一个监听器已注册到事件1、6、12和17,当它注销时,需要有人告诉我需要在这4个EventGenerators上调用removeListener。这是“我的想法”中的第3点。 - user1803551
所以,如果您需要注册或注销所有事件类型,则可视化效果不正确。您应该在同一列的所有单元格上具有X或无内容。对吧? - Davide Lorenzo MARINO
一个事件不是一个事件类型。按钮是一个事件生成器。点击是一个事件。第二个按钮是第二个事件生成器。如果在第一个按钮上进行了点击,则类型为“来自按钮1”,如果在第二个按钮上进行了点击,则类型为“来自按钮2”,但是可以在同一个按钮上发生第二次点击,这是相同类型的新事件,例如在按钮1上进行第二次点击的类型为“来自按钮1”。 - Davide Lorenzo MARINO
显示剩余7条评论

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
    }
}

这不保留排序要求。(1) 对于每个事件,监听器将按照添加到数据结构的顺序调用。(2) 监听器具有全局排序。 - user1803551
@user1803551 我更新了使用的数据结构,以便保留插入顺序。 - Spotted
仍然没有全局排序的侦听器。您正在将侦听器与特定事件相关联,而不是与事件类型相关联。事件是事件类型的实例,并且是“一次性使用”的。您可能希望将关系更改为事件类。 - user1803551
@user1803551 好的,我更新了我的答案,通过存储事件类型而不是实例来完成。我不理解你所说的全局排序的意义:因为我只有一个集合,所有监听器已经按插入顺序或者?全局排序了。 - Spotted
请看我在"My thoughts"中的第二点。监听器需要全局按插入顺序排序(或者我可以尝试按ID排序),而不仅仅是按事件类型排序。这就是为什么这不是一个简单的问题(另一个答案也错过了这一点)。如果没有这个要求,一个简单的multimap就足够了。它可以通过维护另一个List来实现,但我正在寻找一个数据结构,它可以处理所有这些。 - user1803551

0

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

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

键和每个键的监听器都是有序的。

理论考虑

给定一个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集合框架提供了几种保持插入顺序的实现。对于映射,只有LinkedHashMap是可行的。对于值集合,ArrayListLinkedListLinkedHashSet都是可行的。

在上述实现中,Guava为LinkedHashMap提供了multimap实现,其中包括LinkedListLinkedHashSet。后者保证没有重复的值。由于我的监听器是根据设计唯一的(如前所述,它们具有用于equals的唯一int id),因此不需要明确的保证。它们之间也存在迭代顺序差异,但我认为它们仅在监听器可以部分注销(从特定事件类型)时才会表现出来,这不是我的情况。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接