Ereignisgesteuertes Programmieren: Wann lohnt es sich?

19

Ok, ich weiß, der Titel dieser Frage ist fast identisch mit Wann sollte ich ereignisbasierte Programmierung verwenden? Die Antworten auf diese Frage haben mir jedoch nicht bei der Entscheidung geholfen, ob ich Ereignisse in dem speziellen Fall verwenden soll, in dem ich mich befinde.

Ich entwickle eine kleine Anwendung. Es ist eine einfache App, deren Funktionalität zum größten Teil einfach CRUD ist.

Bei bestimmten Ereignissen (beim Ändern bestimmter Daten) muss die Anwendung eine lokale Kopie dieser Daten in eine Datei schreiben. Ich bin mir nicht sicher, wie ich das am besten umsetzen kann. Ich kann:

  • Lösen Sie Ereignisse aus, wenn die Daten geändert werden, und binden Sie eine Antwort (generieren Sie die Datei) an solche Ereignisse. Alternativ können Sie das Beobachtermuster implementieren. Das scheint unnötig kompliziert zu sein.
  • Rufen Sie den dateierzeugenden Code direkt aus dem Code auf, der die Daten ändert. Viel einfacher, aber es scheint falsch, dass die Abhängigkeit auf diese Weise sein sollte, das heißt, es scheint falsch, dass die Kernfunktionalität der App (Code, der Daten ändert) mit diesem zusätzlichen Vorteil (Code, der eine Sicherungsdatei generiert) gekoppelt werden sollte. Ich weiß jedoch, dass sich diese App nicht zu einem Zeitpunkt entwickeln wird, an dem diese Kopplung ein Problem darstellt.

Was ist in diesem Fall der beste Ansatz?

abl
quelle
2
Ich würde sagen, implementieren Sie nichts selbst - verwenden Sie einfach einen vorhandenen Eventbus. Dies wird das Leben viel einfacher machen ...
Boris die Spinne
Ereignisgesteuerte Programmierung ist von Natur aus asynchron. Ereignisse können in der von Ihnen beabsichtigten Reihenfolge oder in einer anderen Reihenfolge oder überhaupt nicht eintreten oder nicht eintreten. Wenn Sie mit dieser zusätzlichen Komplexität umgehen können, entscheiden Sie sich dafür.
Pieter B
Ereignisgesteuert bedeutet in der Regel, dass Ihr Code als Rückruf bereitgestellt wird und diese auf nicht vorhersehbare Weise von einer anderen Stelle aus aufgerufen werden. Ihre Beschreibung klingt eher so, als ob Sie mehr tun müssten , wenn in Ihrem Code etwas Bestimmtes passiert, als es eine naive Implementierung erfordern würde. Geben Sie einfach die zusätzlichen Anrufe ein.
Thorbjørn Ravn Andersen
Es gibt einen Unterschied zwischen ereignisgesteuert und ereignisbasiert. Sehen Sie sich zum Beispiel die zum Nachdenken anregende .NET Rocks-Podcast-Episode 355 mit Ted Faison an. Ted Faison bringt Ereignisse an die Grenze! ( direkter Download-URL ) und das Buch Eventbasierte Programmierung: Events an ihre Grenzen bringen .
Peter Mortensen
Das Interview mit Ted Faison beginnt um 13 Minuten und 10 Sekunden.
Peter Mortensen

Antworten:

31

Befolgen Sie das KISS-Prinzip: Halten Sie es einfach, dumm oder das YAGNI-Prinzip: Sie werden es nicht brauchen.

Sie können den Code wie folgt schreiben:

void updateSpecialData() {
    // do the update.
    backupData();
}

Oder Sie können Code schreiben wie:

void updateSpecialData() {
     // do the update.
     emit SpecialDataUpdated();
}

void SpecialDataUpdatedHandler() {
     backupData();
}

void configureEventHandlers() {
     connect(SpecialDataUpdate, SpecialDataUpdatedHandler);
}

Wenn kein zwingender Grund vorliegt, etwas anderes zu tun, folgen Sie dem einfacheren Weg. Techniken wie die Ereignisbehandlung sind leistungsstark, erhöhen jedoch die Komplexität Ihres Codes. Es erfordert mehr Code, um zu funktionieren, und es erschwert die Verfolgung der Code-Vorgänge.

Ereignisse sind in der richtigen Situation sehr kritisch (stellen Sie sich vor, Sie würden versuchen, die Benutzeroberfläche ohne Ereignisse zu programmieren!). Verwenden Sie sie jedoch nicht, wenn Sie stattdessen KISS oder YAGNI können.

Winston Ewert
quelle
Mir gefällt besonders die Tatsache, dass Sie erwähnt haben, dass das Auslösen von Ereignissen beim Ändern von Daten nicht trivial ist.
NoChance
13

Das von Ihnen beschriebene Beispiel für einfache Daten, bei denen die Änderung einen gewissen Effekt auslöst, kann mit dem Beobachter-Entwurfsmuster perfekt implementiert werden :

  • Dies ist einfacher zu implementieren und zu warten als vollständiger ereignisgesteuerter Code.
  • Die Kopplung zwischen Subjekt und Betrachter kann abstrakt sein, was die Trennung der Anliegen erleichtert.
  • Es ist ideal für eine bis viele Beziehung (die Probanden haben einen oder mehrere Beobachter).

Ein ereignisgesteuerter Ansatz lohnt sich für komplexere Szenarien, in denen viele verschiedene Wechselwirkungen auftreten können, in einem Many-to-Many-Kontext oder wenn Kettenreaktionen vorgesehen sind (z. B. informiert ein Subjekt einen Beobachter, der in einigen Fällen die Daten ändern möchte) Fach oder andere Fächer)

Christophe
quelle
1
Ich bin verwirrt, ist Beobachter nicht nur eine Möglichkeit, Ereignisse umzusetzen?
Svick
1
@svick Ich glaube nicht. In der ereignisgesteuerten Programmierung haben Sie eine Hauptschleife, die Ereignisse in einer Beziehung von vielen zu vielen verarbeitet, mit entkoppelten Absendern und Beobachtern. Ich denke, der Beobachter kann durch die Verarbeitung eines bestimmten Ereignistyps einen Beitrag leisten, aber Sie können nicht das gesamte Spektrum der EDV nur mit einem Beobachter erreichen. Ich denke, die Verwirrung rührt von der Tatsache her, dass in ereignisgesteuerter Software Beobachter manchmal auf der obersten
Christophe,
5

Wie Sie sagen, sind Ereignisse ein großartiges Werkzeug, um die Kopplung zwischen Klassen zu verringern. Es kann zwar erforderlich sein, zusätzlichen Code in einigen Sprachen zu schreiben, ohne dass Ereignisse unterstützt werden, dies verringert jedoch die Komplexität des Gesamtbilds.

Ereignisse sind wohl eines der wichtigsten Werkzeuge in OO (laut Alan Kay kommunizieren Objekte durch das Senden und Empfangen von Nachrichten ). Wenn Sie eine Sprache verwenden, die Ereignisse unterstützt, oder Funktionen als erstklassige Bürger behandelt, ist ihre Verwendung ein Kinderspiel.

Sogar in Sprachen ohne eingebaute Unterstützung ist die Menge an Boilerplate für etwas wie das Observer-Muster ziemlich gering. Möglicherweise finden Sie irgendwo eine anständige generische Eventing-Bibliothek, die Sie in all Ihren Anwendungen verwenden können, um das Boilerplate zu minimieren. (Ein generischer Ereignisaggregator oder Ereignismediator ist in nahezu jeder Anwendung nützlich.)

Lohnt es sich in einer kleinen Anwendung? Ich würde definitiv ja sagen .

  • Wenn Sie die Klassen voneinander entkoppelt halten, bleibt Ihr Klassenabhängigkeitsdiagramm übersichtlich.
  • Klassen ohne konkrete Abhängigkeiten können isoliert getestet werden, ohne dass andere Klassen in den Tests berücksichtigt werden.
  • Klassen ohne konkrete Abhängigkeiten erfordern weniger Komponententests für eine vollständige Abdeckung.

Wenn Sie denken "Oh, aber es ist wirklich nur eine sehr kleine Anwendung, es ist wirklich nicht so wichtig" , bedenken Sie:

  • Kleine Anwendungen werden manchmal später mit größeren Anwendungen kombiniert.
  • Kleine Anwendungen enthalten wahrscheinlich mindestens einige Logikkomponenten oder Komponenten, die später möglicherweise in anderen Anwendungen wiederverwendet werden müssen.
  • Die Anforderungen für kleine Anwendungen können sich ändern, sodass eine Umgestaltung erforderlich ist. Dies ist einfacher, wenn vorhandener Code entkoppelt wird.
  • Weitere Funktionen können später hinzugefügt werden, sodass der vorhandene Code erweitert werden muss. Dies ist auch viel einfacher, wenn der vorhandene Code bereits entkoppelt ist.
  • Bei lose gekoppeltem Code dauert das Schreiben im Allgemeinen nicht viel länger als bei fest gekoppeltem Code. Bei eng gekoppeltem Code dauert das Umgestalten und Testen jedoch viel länger als bei lose gekoppeltem Code.

Insgesamt sollte die Größe einer Anwendung kein entscheidender Faktor dafür sein, ob Klassen lose gekoppelt bleiben sollen. SOLID-Prinzipien gelten nicht nur für große Anwendungen, sondern für Software und Codebasen in jeder Größenordnung.

Tatsächlich sollte die Zeitersparnis beim Unit-Testen Ihrer lose gekoppelten Klassen für sich genommen die zusätzliche Zeit ausgleichen, die zum Entkoppeln dieser Klassen aufgewendet wurde.

Ben Cottrell
quelle
2

Das Beobachtermuster kann viel kleiner implementiert werden, als der Wikipedia-Artikel (oder das GOF-Buch) es beschreibt, vorausgesetzt, Ihre Programmiersprachen unterstützen so etwas wie "Rückrufe" oder "Delegierte". Übergeben Sie einfach eine Callback-Methode in Ihren CRUD-Code (die Observer-Methode, die entweder eine generische Methode zum Schreiben in eine Datei oder eine leere Methode sein kann). Anstelle von "Ereignisauslösung" rufen Sie einfach diesen Rückruf auf.

Der resultierende Code ist nur minimal komplexer als der direkte Aufruf des dateierzeugenden Codes, jedoch ohne die Nachteile einer engen Kopplung nicht verwandter Komponenten.

Das bringt Ihnen "das Beste aus beiden Welten", ohne die Entkopplung für "YAGNI" zu opfern.

Doc Brown
quelle
Das funktioniert beim ersten Doc, aber wenn das Ereignis eine zweite Sache auslösen muss, muss die Klasse wahrscheinlich auf diese Weise geändert werden. Wenn Sie ein Beobachtermuster verwenden, können Sie beliebig viele neue Verhalten hinzufügen, ohne die ursprüngliche Klasse zur Änderung zu öffnen.
RubberDuck
2
@RubberDuck: Das OP suchte nach einer Lösung, um "unnötige Komplexität zu vermeiden" - wenn unterschiedliche Ereignisse / Verhaltensweisen erforderlich sind, würde er das Beobachtermuster wahrscheinlich nicht als zu komplex betrachten. Ich stimme dem zu, was Sie gesagt haben, wenn die Dinge komplexer werden, würde ein vollständiger Beobachter ihm besser dienen, aber nur dann.
Doc Brown
Eine faire Aussage, aber es fühlt sich für mich wie ein zerbrochenes Fenster an.
RubberDuck
2
@RubberDuck: Das Hinzufügen eines vollständigen Beobachters mit Verleger- / Abonnentenmechanik "nur für den Fall", wenn es nicht wirklich benötigt wird, fühlt sich für mich nach Überentwicklung an - das ist nicht besser.
Doc Brown
1
Ich bin nicht anderer Meinung, dass es über Engineering sein kann. Ich fühle mich wahrscheinlich so, weil es trivial ist, es in meinem Stapel der Wahl zu implementieren. Wie auch immer, wir werden nur zustimmen, nicht zuzustimmen?
RubberDuck