Wie kann die Wartung von ereignisgesteuertem Code vereinfacht werden?

16

Wenn ich eine ereignisbasierte Komponente verwende, habe ich in der Wartungsphase häufig Schmerzen.

Da der ausgeführte Code vollständig aufgeteilt ist, kann es schwierig sein, den gesamten Codeteil zu ermitteln, der zur Laufzeit benötigt wird.

Dies kann zu subtilen und schwer zu debuggenden Problemen führen, wenn jemand neue Ereignishandler hinzufügt.

Bearbeiten von Kommentaren: Selbst mit einigen bewährten Methoden an Bord, z. B. einem anwendungsweiten Event-Bus und Handlern, die das Geschäft an einen anderen Teil der App delegieren, wird der Code in einem bestimmten Moment schwer lesbar, da viele vorhanden sind registrierte Handler von vielen verschiedenen Orten (besonders wahr, wenn es einen Bus gibt).

Dann beginnt das Sequenzdiagramm komplexer zu werden, der Zeitaufwand, um herauszufinden, was gerade passiert, nimmt zu und die Debugsitzung wird chaotisch (Haltepunkt auf dem Handler-Manager, während auf Handlern iteriert wird, besonders erfreulich bei asynchronen Handlern und einigen Filtern darüber).

////////////////
Beispiel

Ich habe einen Dienst, der einige Daten auf dem Server abruft. Auf dem Client haben wir eine Basiskomponente, die diesen Service über einen Callback aufruft. Um den Benutzern der Komponente einen Erweiterungspunkt zur Verfügung zu stellen und eine Kopplung zwischen verschiedenen Komponenten zu vermeiden, werden einige Ereignisse ausgelöst: eines vor dem Senden der Abfrage, eines, wenn die Antwort zurückkommt, und eines, wenn ein Fehler auftritt. Wir haben eine Grundmenge von Handlern, die vorregistriert sind und das Standardverhalten der Komponente liefern.

Jetzt können Benutzer der Komponente (und wir sind auch Benutzer der Komponente) einige Handler hinzufügen, um Änderungen am Verhalten vorzunehmen (Abfragen, Protokolle, Datenanalyse, Datenfilterung, Datenmassage, ausgefallene Benutzeroberflächenanimation, Verkettung mehrerer sequenzieller Abfragen) , was auch immer). Daher müssen einige Handler vor / nach anderen ausgeführt werden, und sie werden an vielen verschiedenen Einstiegspunkten in der Anwendung registriert.

Nach einer Weile kann es vorkommen, dass ein Dutzend oder mehr Handler registriert sind, und die Arbeit damit kann mühsam und gefährlich sein.

Dieser Entwurf entstand, weil die Verwendung der Vererbung zu einem völligen Chaos wurde. Das Ereignissystem wird bei einer Art Komposition verwendet, bei der Sie noch nicht wissen, was Ihre Kompositionen sein werden.

Ende des Beispiels
////////////////

Ich frage mich also, wie andere Leute diese Art von Code angehen. Sowohl beim Schreiben als auch beim Lesen.

Haben Sie Methoden oder Werkzeuge, mit denen Sie solchen Code ohne allzu große Schmerzen schreiben und warten können?

Guillaume
quelle
Sie meinen, neben der Überarbeitung der Logik von Event-Handlern?
Telastyn
Dokumentieren Sie, was los ist.
@Telastyn, ich bin mir nicht sicher, was Sie unter "neben der Überarbeitung der Logik von Event-Handlern" verstehen sollen.
Guillaume
@Thorbjoern: siehe mein update.
Guillaume
3
Klingt es so, als würden Sie nicht das richtige Werkzeug für den Job verwenden? Ich meine, wenn die Reihenfolge wichtig ist, dann sollten Sie nicht in erster Linie einfache Ereignisse für diese Teile der Anwendung verwenden. Grundsätzlich ist es kein typisches Bussystem mehr, wenn die Reihenfolge der Ereignisse in einem typischen Bussystem begrenzt ist, aber Sie verwenden es immer noch als solches. Das heißt, Sie müssen dieses Problem beheben, indem Sie eine zusätzliche Logik hinzufügen, um die Bestellung abzuwickeln. Zumindest, wenn ich Ihr Problem richtig verstehe ..
stijn

Antworten:

7

Ich habe festgestellt, dass die Verarbeitung von Ereignissen mithilfe eines Stapels interner Ereignisse (genauer gesagt einer LIFO-Warteschlange mit willkürlichem Entfernen) die ereignisgesteuerte Programmierung erheblich vereinfacht. Sie können die Verarbeitung eines "externen Ereignisses" in mehrere kleinere "interne Ereignisse" mit einem genau definierten Status dazwischen aufteilen. Weitere Informationen finden Sie in meiner Antwort auf diese Frage .

Hier präsentiere ich ein einfaches Beispiel, das durch dieses Muster gelöst wird.

Angenommen, Sie verwenden Objekt A, um einen Dienst auszuführen, und rufen es zurück, um Sie zu benachrichtigen, wenn er abgeschlossen ist. A ist jedoch so, dass es nach dem Aufrufen Ihres Rückrufs möglicherweise weitere Arbeit leisten muss. Eine Gefahr entsteht, wenn Sie innerhalb dieses Rückrufs entscheiden, dass Sie A nicht mehr benötigen, und Sie es auf die eine oder andere Weise zerstören. Sie werden jedoch von A angerufen. Wenn A nach der Rückkehr Ihres Rückrufs nicht sicher erkennen kann, dass er zerstört wurde, kann es zu einem Absturz kommen, wenn es versucht, die verbleibende Arbeit auszuführen.

ANMERKUNG: Es ist richtig, dass Sie die "Zerstörung" auf eine andere Art und Weise durchführen können, wie z. B. durch Verringern eines Nachzählers. Dies führt jedoch nur zu Zwischenzuständen und zusätzlichem Code und Fehlern bei der Behandlung dieser Zustände. Es ist besser, wenn A einfach aufhört zu arbeiten, nachdem Sie es nicht mehr benötigen, als in einem Zwischenzustand fortzufahren.

In meinem Muster würde ein einfach planen die weitere Arbeit , die sie durch Drücken eines internen Ereignisses (Job) in der Ereignisschleife der LIFO - Warteschlange tun muss, dann gehen Sie den Rückruf rufen, und sofort wieder zu Ereignisschleife . Dieses Stück Code ist keine Gefahr mehr, da A gerade zurückkehrt. Wenn der Rückruf A nicht zerstört, wird der Push-Job schließlich von der Ereignisschleife ausgeführt, um seine zusätzliche Arbeit zu erledigen (nachdem der Rückruf und alle seine Push-Jobs rekursiv erledigt wurden). Wenn der Rückruf dagegen A zerstört, kann die Destruktor- oder Deinit-Funktion von A den Push-Auftrag aus dem Ereignisstapel entfernen, wodurch die Ausführung des Push-Auftrags implizit verhindert wird.

Ambroz Bizjak
quelle
7

Ich denke, eine ordnungsgemäße Protokollierung kann eine große Hilfe sein. Stellen Sie sicher, dass jedes ausgelöste / behandelte Ereignis irgendwo protokolliert wird (Sie können hierfür Protokollierungsframeworks verwenden). Während des Debuggens können Sie in den Protokollen nachlesen, in welcher Reihenfolge der Code ausgeführt wurde, als der Fehler auftrat. Oft hilft dies, die Ursache des Problems einzugrenzen.

Oleksi
quelle
Ja, das ist ein hilfreicher Rat. Die Menge der produzierten Protokolle kann dann erschreckend sein (ich kann mich erinnern, dass es schwierig war, die Ursache für einen Zyklus bei der Ereignisverarbeitung einer sehr komplexen Form zu finden.)
Guillaume
Möglicherweise besteht eine Lösung darin, die interessanten Informationen in einer separaten Protokolldatei zu protokollieren. Außerdem können Sie jedem Ereignis eine UUID zuweisen, damit Sie jedes Ereignis einfacher verfolgen können. Sie können auch ein Berichterstellungstool schreiben, mit dem Sie bestimmte Informationen aus den Protokolldateien extrahieren können. (Oder verwenden Sie alternativ Schalter im Code, um verschiedene Informationen in verschiedenen Protokolldateien zu protokollieren.)
Giorgio
3

Ich frage mich also, wie andere Leute diese Art von Code angehen. Sowohl beim Schreiben als auch beim Lesen.

Das Modell der ereignisgesteuerten Programmierung vereinfacht die Codierung in gewissem Maße. Es wurde wahrscheinlich als Ersatz für große Select (oder case) -Anweisungen entwickelt, die in älteren Sprachen verwendet wurden, und wurde in frühen visuellen Entwicklungsumgebungen wie VB 3 immer beliebter (Zitieren Sie mich nicht in der Historie, ich habe es nicht überprüft)!

Das Modell wird zum Schmerz, wenn die Ereignissequenz wichtig ist und wenn eine Geschäftsaktion auf mehrere Ereignisse aufgeteilt ist. Dieser Prozessstil verletzt die Vorteile dieses Ansatzes. Versuchen Sie unter allen Umständen, den Aktionscode in das entsprechende Ereignis einzukapseln, und lösen Sie keine Ereignisse innerhalb von Ereignissen aus. Das wird dann weit schlimmer als die Spaghetti aus dem GoTo.

Manchmal sind Entwickler bestrebt, GUI-Funktionen bereitzustellen, die eine solche Ereignisabhängigkeit erfordern, aber es gibt wirklich keine Alternative, die wesentlich einfacher ist.

Unterm Strich ist hier, dass die Technik nicht schlecht ist, wenn sie mit Bedacht eingesetzt wird.

Keine Chance
quelle
Gibt es ein alternatives Design, um das Schlimmer als die Spaghetti zu vermeiden?
Guillaume
Ich bin nicht sicher, ob es einen einfachen Weg gibt, ohne das erwartete GUI-Verhalten zu vereinfachen, aber wenn Sie alle Benutzerinteraktionen mit einem Fenster / einer Seite auflisten und für jede genau festlegen, was Ihr Programm tun soll, können Sie Code an einer Stelle gruppieren und direkt Mehrere Ereignisse für eine Gruppe von Subs / Methoden, die für die tatsächliche Verarbeitung der Anforderung verantwortlich sind. Auch das Trennen von Code, der nur der GUI dient, von Code, der die Back-End-Verarbeitung ausführt, kann Abhilfe schaffen.
NoChance
Die Notwendigkeit, diese Nachricht zu posten, ergab sich aus einem Refactoring, als ich von einer vererbten Hölle zu etwas überging, das auf einem Ereignis beruhte. Irgendwann ist es wirklich besser, aber andererseits ist es ziemlich schlecht ... Da ich in einigen GUIs bereits Probleme mit "wild gewordenen Ereignissen" hatte, frage ich mich, was getan werden kann, um die Wartung solcher zu verbessern ereignisgeschnittener Code.
Guillaume
Ereignisgesteuerte Programmierung ist viel älter als VB; Es war in der SunTools-Benutzeroberfläche vorhanden, und zuvor schien ich mich daran zu erinnern, dass es in die Simula-Sprache integriert war.
Kevin Cline
@ Guillaume, ich denke, Sie sind über Engineering den Service. Meine obige Beschreibung basierte hauptsächlich auf GUI-Ereignissen. Benötigen Sie diese Art der Bearbeitung (wirklich)?
NoChance,
3

Ich wollte diese Antwort aktualisieren, da ich nach dem "Abflachen" und "Abflachen" von Kontrollabläufen einige Heureka-Momente erlebt habe und einige neue Gedanken zu diesem Thema formuliert habe.

Komplexe Nebenwirkungen im Vergleich zu komplexen Kontrollabläufen

Was ich herausgefunden habe, ist, dass mein Gehirn komplexe Nebenwirkungen oder komplexe grafische Steuerungsabläufe tolerieren kann, wie Sie sie normalerweise bei der Ereignisbehandlung finden, aber nicht die Kombination aus beiden.

Ich kann leicht über Code nachdenken, der 4 verschiedene Nebenwirkungen verursacht, wenn sie mit einem sehr einfachen Steuerungsfluss angewendet werden, wie dem einer sequenziellen forSchleife. Mein Gehirn kann eine sequenzielle Schleife tolerieren, die Elemente in der Größe ändert und neu positioniert, sie animiert, neu zeichnet und eine Art Hilfsstatus aktualisiert. Das ist leicht zu verstehen.

Ich kann auch einen komplexen Kontrollfluss nachvollziehen, wie er bei kaskadierenden Ereignissen oder beim Durchlaufen einer komplexen graphischen Datenstruktur auftreten kann, wenn dabei nur ein sehr einfacher Nebeneffekt auftritt, bei dem die Reihenfolge keine Rolle spielt, z. B. beim Markieren von Elementen verzögert in einer einfachen sequentiellen Schleife verarbeitet werden.

Ich verliere mich, bin verwirrt und überwältigt, wenn Sie komplexe Kontrollabläufe haben, die komplexe Nebenwirkungen verursachen. In diesem Fall ist es aufgrund des komplexen Kontrollflusses schwierig, im Voraus vorherzusagen, wo Sie enden werden, während es aufgrund der komplexen Nebenwirkungen schwierig ist, genau vorherzusagen, was als Ergebnis und in welcher Reihenfolge passieren wird. Es ist also die Kombination dieser beiden Dinge, die es so unangenehm macht, wenn der Code, auch wenn er gerade einwandfrei funktioniert, so furchterregend ist, ihn zu ändern, ohne Angst vor unerwünschten Nebenwirkungen zu haben.

Komplexe Kontrollabläufe erschweren es, zu überlegen, wann und wo etwas passieren wird. Das wird nur dann wirklich zu Kopfschmerzen, wenn diese komplexen Kontrollabläufe eine komplexe Kombination von Nebenwirkungen auslösen, bei denen es wichtig ist zu verstehen, wann / wo Dinge passieren, wie zum Beispiel Nebenwirkungen, die eine Art von Abhängigkeit von der Reihenfolge haben, in der eine Sache vor der nächsten passieren sollte.

Vereinfachen Sie den Kontrollfluss oder die Nebenwirkungen

Was machen Sie also, wenn Sie auf das oben beschriebene Szenario stoßen, das so schwer zu verstehen ist? Die Strategie besteht darin, entweder den Kontrollfluss oder die Nebenwirkungen zu vereinfachen.

Eine allgemein anwendbare Strategie zur Vereinfachung von Nebenwirkungen ist die Bevorzugung einer verzögerten Verarbeitung. Am Beispiel eines GUI-Ereignisses zur Größenänderung könnte die normale Versuchung darin bestehen, das GUI-Layout erneut anzuwenden, die untergeordneten Widgets neu zu positionieren und die Größe zu ändern, eine weitere Kaskade von Layoutanwendungen auszulösen und die Größe der Hierarchie zu ändern und möglicherweise einige Steuerelemente neu zu zeichnen Einzigartige Ereignisse für Widgets mit benutzerdefiniertem Größenänderungsverhalten, das mehr Ereignisse auslöst, die zu Wer-weiß-Wo usw. führen. Anstatt dies in einem Durchgang oder durch Spammen der Ereigniswarteschlange zu versuchen, besteht eine mögliche Lösung darin, die Widget-Hierarchie herunterzufahren und markieren Sie, für welche Widgets die Layouts aktualisiert werden müssen. Wenden Sie dann in einem späteren, verzögerten Durchlauf, der einen direkten Ablauf der Ablaufsteuerung aufweist, alle Layouts für Widgets an, die dies benötigen. Sie können dann markieren, welche Widgets neu gestrichen werden müssen. Zeichnen Sie die Widgets, die als neu zu zeichnen sind, in einem sequentiellen, zurückgestellten Durchlauf mit einfachem Steuerungsfluss neu.

Dies hat zur Folge, dass sowohl der Steuerungsfluss als auch die Nebenwirkungen vereinfacht werden, da der Steuerungsfluss vereinfacht wird, da keine rekursiven Ereignisse während des Diagrammdurchlaufs kaskadiert werden. Stattdessen treten die Kaskaden in der verzögerten sequentiellen Schleife auf, die dann in einer anderen verzögerten sequentiellen Schleife behandelt werden könnte. Die Nebenwirkungen werden dort einfach, wo es darauf ankommt, da wir während der komplexeren grafischen Steuerungsabläufe lediglich markieren, was von den verzögerten sequentiellen Schleifen verarbeitet werden muss, die die komplexeren Nebenwirkungen auslösen.

Dies ist mit einigem Verarbeitungsaufwand verbunden, kann jedoch auch dazu führen, dass beispielsweise diese verzögerten Durchläufe parallel ausgeführt werden. Auf diese Weise erhalten Sie möglicherweise eine noch effizientere Lösung, als Sie es von Anfang an erwartet haben, wenn die Leistung ein Problem darstellt. Im Allgemeinen sollte die Leistung jedoch in den meisten Fällen keine große Rolle spielen. Am wichtigsten ist, dass dies wie ein strittiger Unterschied erscheint, ich es jedoch viel einfacher gefunden habe, darüber nachzudenken. Es macht es viel einfacher, vorherzusagen, was wann passiert, und ich kann den Wert nicht überschätzen, der darin besteht, leichter nachvollziehen zu können, was vor sich geht.


quelle
2

Was für mich funktioniert hat, ist, dass jede Veranstaltung für sich steht, ohne Bezug zu anderen Veranstaltungen. Wenn sie in asynchroniously kommen, nicht wahr haben eine Sequenz, so zu versuchen , herauszufinden, was in geschieht welcher Reihenfolge sinnlos ist neben unmöglich zu sein.

Am Ende stehen eine Reihe von Datenstrukturen, die von einem Dutzend Threads in unbestimmter Reihenfolge gelesen und geändert sowie erstellt und entfernt werden. Sie müssen äußerst korrekte Multithread-Programmierung durchführen, was nicht einfach ist. Sie müssen auch multithreaded denken, wie in "Mit diesem Ereignis werde ich mir die Daten ansehen, die ich zu einem bestimmten Zeitpunkt habe, ohne Rücksicht darauf, was eine Mikrosekunde zuvor war, ohne Rücksicht darauf, was sie gerade geändert hat und ohne Rücksicht darauf, was die 100 Threads, die darauf warten, dass ich die Sperre aufhebe, damit anstellen werden. Dann werde ich meine Änderungen auf der Grundlage dessen selbst vornehmen und was ich sehe. Dann bin ich fertig. "

Eine Sache, die ich mache, ist, nach einer bestimmten Sammlung zu suchen und sicherzustellen, dass sowohl die Referenz als auch die Sammlung selbst (wenn nicht threadsicher) korrekt gesperrt und mit anderen Daten synchronisiert sind. Je mehr Ereignisse hinzugefügt werden, desto größer wird diese Aufgabe. Aber wenn ich die Beziehungen zwischen Ereignissen verfolgen würde , würde diese Arbeit viel schneller wachsen. Außerdem kann manchmal ein Großteil der Sperren in einer eigenen Methode isoliert werden, wodurch der Code tatsächlich einfacher wird.

Es ist schwierig (aufgrund des Multithreading mit hartem Kern), jeden Thread als eine völlig unabhängige Entität zu behandeln, aber machbar. "Skalierbar" ist möglicherweise das gesuchte Wort. Doppelt so viele Veranstaltungen erfordern nur doppelt so viel Arbeit und vielleicht nur 1,5-mal so viel. Der Versuch, mehr asynchrone Ereignisse zu koordinieren, wird Sie schnell begraben.

RalphChapin
quelle
Ich habe meine Frage aktualisiert. Sie haben vollkommen recht mit Sequenzen und asynchronen Sachen. Wie würden Sie mit einem Fall umgehen, in dem Ereignis X ausgeführt werden muss, nachdem alle anderen Ereignisse verarbeitet wurden? Es sieht so aus, als müsste ich einen komplexeren Handler-Manager erstellen, aber ich möchte auch vermeiden, den Benutzern zu viel Komplexität aufzuerlegen.
Guillaume
Verfügen Sie in Ihrem Datensatz über einen Schalter und einige Felder, die festgelegt werden, wenn Ereignis X gelesen wird. Wenn alle anderen verarbeitet sind, überprüfen Sie den Schalter und wissen, dass Sie mit X umgehen müssen und über die Daten verfügen. Der Switch und die Daten sollten eigentlich für sich stehen. Wenn festgelegt, sollten Sie denken: "Ich muss diesen Job machen", nicht: "Ich muss X übergeben." Nächstes Problem: Woher wissen Sie, dass die Ereignisse durchgeführt werden? und was ist, wenn Sie 2 oder mehr Ereignisse X erhalten? Im schlimmsten Fall können Sie einen sich wiederholenden Wartungsthread ausführen, der die Situation überprüft und von sich aus handeln kann. (3 Sekunden lang keine Eingabe? X-Schalter gesetzt? Dann Shutdown-Code ausführen.
RalphChapin
2

Es hört sich so an, als ob Sie nach State Machines & Event Driven Activities suchen .

Möglicherweise möchten Sie sich jedoch auch das Beispiel für einen State Machine-Markup-Workflow ansehen .

Hier erhalten Sie einen kurzen Überblick über die Implementierung der Zustandsmaschine. Ein Zustandsautomaten- Workflow besteht aus Zuständen. Jeder Status besteht aus einem oder mehreren Ereignishandlern. Jeder Ereignishandler muss als erste Aktivität eine Verzögerung oder eine IEventActivity enthalten. Jeder Ereignishandler kann auch eine SetStateActivity-Aktivität enthalten, die zum Übergang von einem Zustand in einen anderen verwendet wird.

Jeder Zustandsmaschinen-Workflow verfügt über zwei Eigenschaften: InitialStateName und CompletedStateName. Wenn eine Instanz des Zustandsautomaten-Workflows erstellt wird, wird sie in die InitialStateName-Eigenschaft eingefügt. Wenn die Statusmaschine die CompletedStateName-Eigenschaft erreicht, wird die Ausführung beendet.

Yusubov
quelle
2
Während dies theoretisch die Frage beantworten mag, wäre es vorzuziehen , die wesentlichen Teile der Antwort hier aufzunehmen und den Link als Referenz bereitzustellen.
Thomas Owens
Wenn Sie jedoch Dutzende von Handlern an jeden Status gebunden haben, sind Sie nicht in der Lage zu verstehen, was vor sich geht ... Und jede ereignisbasierte Komponente wird möglicherweise nicht als Statusmaschine bezeichnet.
Guillaume
1

Ereignisgesteuerter Code ist nicht das eigentliche Problem. Ich habe kein Problem damit, der Logik in geradem Code zu folgen, in dem Rückrufe explizit definiert oder Inline-Rückrufe verwendet werden. Zum Beispiel sind Rückrufe im Generator-Stil in Tornado sehr einfach zu befolgen.

Was wirklich schwer zu debuggen ist, sind dynamisch generierte Funktionsaufrufe. Das (Anti?) Muster, das ich die Call-Back-Factory aus der Hölle nennen würde. Diese Art von Funktionsfabriken ist jedoch im herkömmlichen Ablauf genauso schwer zu debuggen.

vartec
quelle