Ereignis-Listener-Muster in der API - Was sollte das zweimalige Hinzufügen desselben Listeners bewirken?

8

Beim Entwerfen einer API, die eine Schnittstelle zum Abhören von Ereignissen bereitstellt, gibt es anscheinend zwei widersprüchliche Methoden zum Behandeln von Aufrufen zum Hinzufügen / Entfernen von Listenern:

  1. Bei mehreren Aufrufen von addListener wird nur ein einziger Listener hinzugefügt (wie beim Hinzufügen zu einem Set). kann mit einem einzigen Aufruf von removeListener entfernt werden.

  2. Bei mehreren Aufrufen von addListener wird jedes Mal ein Listener hinzugefügt (z. B. zum Hinzufügen zu einer Liste). muss durch mehrere Aufrufe von removeListener ausgeglichen werden.

Ich habe jeweils ein Beispiel gefunden: (1) - Der Aufruf von DOM addEventListener in Browsern fügt Listener nur einmal hinzu, ignoriert stillschweigend Anfragen, denselben Listener ein zweites Mal hinzuzufügen, und (2) - jQuery- .onVerhalten fügt Listener mehrmals hinzu .

Die meisten anderen Listener-APIs scheinen (2) zu verwenden, z. B. SWT- und Swing-Ereignis-Listener. Wenn (1) ausgewählt ist, stellt sich auch die Frage, ob es stillschweigend oder mit einem Fehler fehlschlagen soll, wenn derselbe Listener zweimal hinzugefügt werden muss.

In meinen Implementierungen bleibe ich eher bei (2), da es eine übersichtlichere Setup- / Teardown-Schnittstelle bietet und Fehler aufdeckt, bei denen das Setup unbeabsichtigt zweimal durchgeführt wird und mit den meisten Implementierungen übereinstimmt, die ich gesehen habe.

Dies führt mich zu meiner Frage: Gibt es eine bestimmte Architektur oder ein anderes zugrunde liegendes Design, das sich besser für die andere Implementierung eignet? (dh: warum existiert das andere Muster?)

Krease
quelle
1
Zur Verdeutlichung: Überlegen Sie addListener(foo); addListener(foo); addListener(bar);. Fügt Ihr Fall Nr. 1 eins foound eins hinzu baroder nur bar(dh barüberschreibt fooals Hörer)? Würde Fall 2 foozweimal oder einmal feuern?
Apsiller
Die Implementierungen von Fall Nr. 1, mit denen ich im Allgemeinen vertraut bin, beziehen sich auf Referenz - wenn foo== bar, dann würde es überschreiben, andernfalls würde es eins foound eins barals Zuhörer haben. Wenn es immer überschrieben würde, wäre es keine Menge, sondern ein einzelnes Objekt, das einen Beobachter darstellt.
Krease
2
(2) zeigt keine Fehler an, solange Sie nicht verbieten, denselben Listener zweimal hinzuzufügen - und dann gibt es keinen wirklichen Unterschied zu (1).
Doc Brown
Die Auswahl sollte von den Anforderungen Ihrer API-Benutzer abhängen. Daher ist es normalerweise am besten, einen von ihnen zu fragen. Wenn Sie jetzt eine Entwurfsentscheidung treffen und einer der Benutzer Ihre API verwendet und Ihnen mitteilt, dass der Entwurf für ihn nicht gut funktioniert, haben Sie dann die Möglichkeit, diese Entscheidung später zu ändern?
Doc Brown
@DocBrown - In dem speziellen Fall, aus dem ich die Frage gestellt habe, haben wir nicht viele Optionen zum Ändern. Ich weiß, dass es keine große Sache ist, die eine oder andere Option zu verwenden, daher ist die Frage eher eine konzeptionelle - gibt es Gründe, die auf Architektur / Design / Zuverlässigkeit beruhen (dh neben den Benutzerpräferenzen), ein Muster über das andere zu wählen ?
Krease

Antworten:

2

Wenn Sie einige Ereignisse haben, bei denen Probleme beim Verwalten des Hinzufügens / Entfernens auftreten, würde ich mit dem Hinzufügen von IDs beginnen.

Hinzufügen eines Listeners gibt eine ID zurück, die Klasse, die sie hinzugefügt hat, verfolgt die IDs für die hinzugefügten Listener, und wenn sie entfernt werden müssen, ruft sie remove listener mit dieser / diesen eindeutigen IDs auf.

Dies gibt den Verbrauchern die Kontrolle, damit sie das Prinzip des geringsten Erstaunens im Verhalten einhalten können.

Dies bedeutet, dass dasselbe zweimal hinzugefügt wird, zweimal hinzugefügt wird, für jedes eine andere ID angegeben wird und durch Entfernen nach ID nur die mit dieser ID verknüpfte ID entfernt wird. Jeder, der die API verwendet, würde dieses Verhalten erwarten, wenn er die Sigs sieht.

Ein weiterer Zusatz, der gegen YAGNI verstößt, wäre ein GetIds, bei dem Sie ihm einen Listener übergeben und eine Liste der mit diesem Listener verknüpften IDs zurückgeben, wenn er in der Lage ist, geeignete Gleichheitsprüfungen zu erhalten, obwohl dies von Ihrer Sprache abhängt: Ist es Referenzgleichheit? , Typengleichheit, Wertgleichheit usw.? Sie müssen hier vorsichtig sein, da Sie möglicherweise IDs zurückgeben, die der Verbraucher nicht entfernen oder einmischen sollte. Daher wird diese Exposition gefährlich und ist schlecht beraten und sollte unnötig sein. GetIDs sind jedoch eine mögliche Ergänzung, wenn Sie Glück haben.

Jimmy Hoffa
quelle
Es gibt nicht unbedingt ein Problem, bei dem es Probleme beim Hinzufügen / Entfernen gibt (da die Frage für jedes Programm gelten sollte, nicht nur für das, an dem ich arbeite), obwohl ich definitiv sehe, wie dieses Muster deutlich machen würde, dass die API ist Verwenden von Option 2 (oder einer geringfügigen Variation davon, bei der das Entfernen nach ID statt nach Listener erfolgt)
Krease
Wenn ein Abonnement ein Objekt zurückgibt, das zur Abmeldung verwendet werden kann, ist meiner Meinung nach der richtige Ansatz. In der objektorientierten Frameworks mit IDisposable, Autocloseableoder andere solche Schnittstelle sollten das Abmelde Objekt diese Schnittstelle in Thread-sicheren Art und Weise implementieren (immer möglich - wenn nichts anderes durch die Teilnehmer innerhalb des Abmelde - Objekts platzieren selbst und mit seinen Abmeldeverfahren entkräften, Referenz und Abonnementanforderungen durchsuchen gelegentlich die Abonnementliste nach toten Abonnements.
Supercat
3

Zunächst würde ich einen Ansatz wählen, bei dem die Reihenfolge, in der die Listener hinzugefügt werden, genau der Reihenfolge entspricht, in der sie aufgerufen werden, wenn die zugehörigen Ereignisse ausgelöst werden. Wenn Sie sich für (1) entscheiden, bedeutet dies, dass Sie ein geordnetes Set verwenden, nicht nur ein Set.

Zweitens sollten Sie ein allgemeines Entwurfsziel klarstellen: Soll Ihre API eher einer "Absturz-Früh" -Strategie oder einer "Fehlerverzeihungs" -Strategie folgen? Dies hängt von der Nutzungsumgebung und den Nutzungsszenarien Ihrer API ab. Im Allgemeinen (bei der Entwicklung von hauptsächlich Desktop-Apps) bevorzuge ich "früh abstürzen", aber manchmal ist es besser, Fehler zu tolerieren, um die Verwendung einer API reibungsloser zu gestalten. Die Anforderungen, beispielsweise in eingebettete Apps oder Server-Apps, können unterschiedlich sein. Vielleicht diskutieren Sie dies mit einem Ihrer potenziellen API-Benutzer?

Verwenden Sie für eine "Crash Early" -Strategie (2), aber verbieten Sie das zweimalige Hinzufügen desselben Listeners. Wenn ein Listener erneut hinzugefügt wird, wird eine Ausnahme ausgelöst. Wirf auch eine Ausnahme aus, wenn versucht wird, einen Listener zu entfernen, der nicht in der Liste enthalten ist.

Wenn Sie der Meinung sind, dass eine Strategie zur Fehlerverzeihung in Ihrem Fall angemessener ist, können Sie dies auch tun

  • Ignorieren Sie das doppelte Hinzufügen desselben Listeners zur Liste - dies ist Option (1) - oder

  • Hängen Sie es wie in (2) an die Liste an, damit es beim Auslösen der Ereignisse zweimal aufgerufen wird

  • oder Sie hängen es an, rufen aber im Falle einer Ereignisauslösung nicht zweimal denselben Listener an

Beachten Sie, dass das Entfernen des Listeners dem entsprechen sollte. Wenn Sie das doppelte Hinzufügen ignorieren, sollten Sie auch das doppelte Entfernen ignorieren. Wenn Sie zulassen, dass derselbe Listener zweimal hinzugefügt wird, sollte klar sein, welcher der beiden Listener-Einträge entfernt wird, wenn einer anruft removeListener(foo). Die letzte der drei Aufzählungszeichen ist höchstwahrscheinlich der am wenigsten fehleranfällige Ansatz unter diesen Vorschlägen. Im Falle einer Strategie zur "Fehlerverzeihung" würde ich mich dem anschließen.

Doc Brown
quelle
Meine bisherige Hauptstrategie bestand darin, ein ähnliches Muster wie die Umgebung zu verwenden - dh das häufigste Muster, das im Rest des Codes / der Sprache verwendet wird, in den ich mich integriere. Die Strategie "Früh abstürzen" würde definitiv viele potenzielle Probleme auffangen, aber ich habe dies in der Praxis noch nie gesehen. Daher könnte es für einen API-Benutzer überraschend sein (obwohl ich darüber nachdenken würde, je mehr ich darüber nachdenke eine 'gute' Überraschung, da es helfen würde, Fehler zu fangen)
Krease
@ Chris: Ich bin mir ziemlich sicher, dass Sie viel "früh abstürzen" gesehen haben. In den meisten modernen Mainstream-Sprachen treten beispielsweise Ausnahmen auf, wenn Sie versuchen, außerhalb von Array-Grenzen zu schreiben, Zeichenfolgen in Zahlen zu konvertieren, die nicht konvertierbar sind, und so weiter.
Doc Brown
Ich bezog mich speziell im Zusammenhang mit dem
Hörermuster darauf