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?
quelle
Antworten:
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.
quelle
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.
quelle
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.
quelle
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
for
Schleife. 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
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.
quelle
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.
quelle
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.
quelle