Das Beobachtermuster - weitere Überlegungen und verallgemeinerte C ++ - Implementierung

7

Ein C ++ - MVC-Framework, das ich schreibe, benutzt das Beobachtermuster stark. Ich habe das entsprechende Kapitel in Design Patterns (GoF, 1995) gründlich gelesen und eine Vielzahl von Implementierungen in Artikeln und vorhandenen Bibliotheken (einschließlich Boost) betrachtet.

Aber als ich das Muster implementierte, konnte ich das Gefühl nicht los, dass es einen besseren Weg geben musste - mein Client-Code beinhaltete Zeilen und Ausschnitte, die ich für das Muster selbst gedacht hätte, wenn ich nur einen finden könnte Möglichkeit, einige C ++ - Einschränkungen zu überwinden. Außerdem erschien meine Syntax nie so elegant wie in der ExtJs-Bibliothek:

%Vor%

Also habe ich beschlossen, weitere Untersuchungen durchzuführen, um zu einer verallgemeinerten Implementierung zu gelangen, während ich Code-Eleganz, Lesbarkeit und Leistung priorisiert habe. Ich glaube, ich habe den 5. Jackpot geknackt.

Die tatsächliche Implementierung , genannt gxObserver , ist auf GitHub verfügbar; Es ist gut dokumentiert und die Readme-Dateien buchstabieren die Profis sowie die Nachteile. Seine Syntax lautet:

%Vor%

Nachdem ich zu einer übermäßigen Arbeit geworden war, hatte ich das Gefühl, dass ich nur meine Ergebnisse mit der SO-Community teilen sollte. Also unten werde ich diese Frage beantworten:

  

Welche zusätzlichen Überlegungen (zu den in Entwurfsmustern dargestellten) sollten Programmierer bei der Implementierung des Beobachtermusters berücksichtigen?

Wenn Sie sich auf C ++ konzentrieren, werden viele der folgenden Punkte in jeder Sprache angewendet.

Bitte beachten Sie: Da SO die Antworten auf 30000 Wörter begrenzt, musste meine Antwort in zwei Teilen bereitgestellt werden, aber manchmal erscheint zuerst die zweite Antwort (die mit "Themen" beginnt). Teil 1 der Antwort beginnt mit dem Klassendiagramm von Design Patterns.

    
Izhaki 31.01.2013, 19:40
quelle

2 Antworten

11

(Beginn von Teil I)

Voraussetzungen

Es ist nicht alles über den Status

Design Patterns bindet das Beobachtermuster an einen Objektstatus. Wie im obigen Klassendiagramm (aus Design Patterns) ersichtlich, kann der Status eines Subjekts mit der Methode SetState() festgelegt werden. nach dem Zustandswechsel wird das Subjekt alle Beobachter benachrichtigen; Dann können Beobachter den neuen Status mit der Methode GetState() abfragen.

Allerdings ist GetState() keine tatsächliche Methode in der Subjekt-Basisklasse. Stattdessen stellt jedes konkrete Thema seine eigenen spezialisierten Zustandsmethoden zur Verfügung. Ein tatsächlicher Code könnte so aussehen:

%Vor%

Was ist ein Objektstatus? Wir definieren es als die Sammlung von Statusvariablen - Membervariablen, die persistent sein müssen (für die spätere Wiederverwendung). Zum Beispiel könnten sowohl BorderWidth als auch FillColour Zustandsvariablen einer Figure-Klasse sein.

Die Idee, dass wir mehr als eine Zustandsvariable haben können - und somit der Zustand eines Objekts kann sich auf mehrere Arten ändern - ist wichtig. Dies bedeutet, dass Subjekte wahrscheinlich mehr als eine Art von Statusänderungsereignis auslösen. Es erklärt auch, warum es wenig Sinn macht, eine GetState() -Methode in der Subjekt-Basisklasse zu haben.

Aber ein Beobachtermuster, das nur mit Zustandsänderungen umgehen kann, ist unvollständig. Beobachter beobachten häufig nachrichtenlose Meldungen, d. h. solche, die nicht auf den Zustand bezogen sind. Zum Beispiel die KeyPress oder MouseMove OS-Ereignisse; oder Ereignisse wie BeforeChildRemove , was eindeutig keine tatsächliche Zustandsänderung bedeutet. Diese staatenlosen Ereignisse sind ausreichend, um einen Push-Mechanismus zu rechtfertigen - wenn Beobachter die Änderungsinformationen des Subjekts nicht abrufen können, müssen alle Informationen mit der Benachrichtigung geliefert werden (mehr dazu in Kürze).

Es wird viele Ereignisse geben

Es ist leicht zu sehen, wie ein Subjekt im wirklichen Leben viele Arten von Ereignissen auslösen kann; Ein kurzer Blick auf die ExtJs-Bibliothek zeigt, dass einige Klassen mehr als 30 Events anbieten. Daher muss ein verallgemeinertes Subjekt-Beobachter-Protokoll integrieren, was Design Patterns ein "Interesse" nennt - es erlaubt Beobachtern, ein bestimmtes Ereignis zu abonnieren, und Subjekte, dieses Ereignis nur für interessierte Beobachter auszulösen.

%Vor%

Es könnte viele-zu-viele sein

Ein einzelner Beobachter kann das gleiche Ereignis aus einer Vielzahl von Subjekten beobachten (wodurch die Beziehung zwischen Beobachter und Subjekt viele-zu-viele wird). Beispielsweise kann ein Eigenschafteninspektor eine Änderung der gleichen Eigenschaft vieler ausgewählter Objekte abhören. Wenn Beobachter interessiert sind, welches Thema die Benachrichtigung gesendet hat, muss die Benachrichtigung den Absender enthalten:

%Vor%

Es ist jedoch erwähnenswert, dass Beobachter sich in vielen Fällen nicht um die Absenderidentität kümmern. Zum Beispiel, wenn das Subjekt ein Singleton ist oder wenn die Handhabung des Ereignisses durch den Beobachter nicht abhängig ist. Anstatt zu zwingen , dass der Absender Teil eines Protokolls ist, sollten wir zulassen , dass es dem Programmierer überlassen bleibt, ob er den Absender buchstabieren soll oder nicht.

Beobachter

Event Handler

Die Methode des Beobachters, die Ereignisse behandelt (dh der Ereignishandler ), kann zwei Formen haben: überschrieben oder willkürlich. In diesem Abschnitt wird auf die kritische und komplexe Rolle der Implementierung der Beobachter eingegangen.

Überschriebener Handler

Ein überschriebener Handler ist die von Design Patterns vorgeschlagene Lösung. Die Basis-Subject-Klasse definiert eine virtuelle OnEvent() -Methode und Unterklassen überschreiben sie:

%Vor%

Beachten Sie, dass wir bereits die Idee berücksichtigt haben, dass Themen typischerweise mehr als eine Art von Ereignis auslösen. Aber die Behandlung aller Ereignisse (insbesondere wenn es Dutzende von ihnen gibt) in der Methode OnEvent ist unhandlich - wir können besseren Code schreiben, wenn jedes Ereignis in seinem eigenen Handler behandelt wird; effektiv macht dies OnEvent zu einem Event-Router für andere Handler:

%Vor%

Der Vorteil eines überladenen (Basisklassen-) Handlers ist, dass er einfach zu implementieren ist. Ein Subskribent, der ein Subjekt abonniert, kann dies tun, indem er einen Verweis auf sich selbst liefert:

%Vor%

Dann behält das Subjekt nur eine Liste von Observer -Objekten und der Feuercode könnte so aussehen:

%Vor%

Der Nachteil des überschriebenen Handlers ist, dass seine Signatur fixiert ist, was die Übergabe zusätzlicher Parameter (in einem Push-Modell) schwierig macht. Darüber hinaus muss der Programmierer für jedes Ereignis zwei Bits Code speichern: den Router ( OnEvent ) und den tatsächlichen Handler ( OnSizeChanged ).

Arbitrary Handler

Der erste Schritt bei der Überwindung der Defizite eines überschriebenen OnEvent -Handlers ist ..., indem Sie nicht alles haben! Es wäre schön, wenn wir dem Betreffenden sagen könnten, welche Methode für jedes Ereignis zuständig ist. So ähnlich:

%Vor%

Beachten Sie, dass bei dieser Implementierung unsere Klasse nicht mehr von der Klasse Observer erben muss; Tatsächlich brauchen wir überhaupt keine Observer-Klasse. Diese Idee ist keine neue, sie wurde ausführlich in Herb Sutters Artikel von Dr. Dobbs aus dem Jahr 2003 mit dem Titel 'Generalizing Observer' . Aber die Implementierung von beliebigen Callbacks in C ++ ist kein einfacher Prozess function facility in seinem Artikel, aber leider wurde ein Schlüsselproblem in seinem Vorschlag nicht vollständig gelöst.Das Problem und seine Lösung werden im Folgenden beschrieben.

Da C ++ keine nativen Delegaten bereitstellt, müssen wir Member Function Pointer (MFP) verwenden. MFPs in C ++ sind Klassenfunktionszeiger und keine Objektfunktionszeiger, daher mussten wir die Subscribe -Methode sowohl mit &ConcreteObserver::OnSizeChanged (Der MFP) als auch mit this (der Objektinstanz) bereitstellen. Wir nennen diese Kombination einen Delegierten .

  

Mitgliedsfunktionszeiger + Objektinstanz = Delegieren

Die Implementierung der Klasse Subject darf beruht auf der Möglichkeit, Delegierte zu vergleichen. Zum Beispiel in Fällen, in denen wir ein Ereignis an einen bestimmten Delegierten auslösen möchten oder wenn wir einen bestimmten Delegierten abmelden möchten. Wenn der Handler kein virtueller ist und zu der abonnierenden Klasse gehört (im Gegensatz zu einem Handler, der in einer Basisklasse deklariert ist), sind die Delegierten wahrscheinlich vergleichbar. Aber in den meisten anderen Fällen macht der Compiler oder die Komplexität der Vererbungsstruktur (virtuelle oder mehrfache Vererbung) sie unvergleichbar. Don Clugston hat einen fantastischen ausführlichen Artikel zu diesem Thema geschrieben Problem, in dem er auch eine C ++ - Bibliothek zur Verfügung stellt, die das Problem überwindet; Obwohl sie nicht standardkonform sind, arbeitet die Bibliothek mit so ziemlich jedem Compiler da draußen.

Es lohnt sich zu fragen, ob virtuelle Event-Handler etwas sind, was wir wirklich brauchen; Das heißt, ob wir ein Szenario haben, in dem eine Beobachterunterklasse das Ereignisbehandlungsverhalten ihrer (konkreten Beobachter-) Basisklasse überschreiben (oder erweitern) möchte. Leider ist die Antwort, dass dies gut möglich ist. Eine verallgemeinerte Beobachterimplementierung sollte also virtuelle Handler erlauben, und wir werden bald ein Beispiel dafür sehen.

Das Aktualisierungsprotokoll

Der Implementierungspunkt 7 von

Design Patterns beschreibt die Pull-vs-Push-Modelle. Dieser Abschnitt erweitert die Diskussion.

Ziehen Sie

Beim Pull-Modell sendet das Subjekt minimale Benachrichtigungsdaten und der Beobachter muss dann weitere Informationen vom Subjekt abrufen.

Wir haben bereits festgestellt, dass das Pull-Modell für statusfreie Ereignisse wie BeforeChildRemove nicht funktioniert. Es ist vielleicht auch erwähnenswert, dass der Programmierer mit dem Pull-Modell Codezeilen zu jedem Ereignishandler hinzufügen muss, die mit dem Push-Modell nicht existieren würden:

%Vor%

Eine weitere Sache, an die wir uns erinnern sollten, ist, dass wir das Pull-Modell mithilfe eines Push-Modells implementieren können, aber nicht umgekehrt. Obwohl das Push-Modell dem Beobachter alle Informationen liefert, die es benötigt, möchte ein Programmierer vielleicht keine Informationen mit bestimmten Ereignissen senden und die Beobachter das Thema für weitere Informationen fragen lassen.

Fester Tastendruck

Bei einem Fixed-Arity-Push-Modell werden die Informationen, die eine Benachrichtigung trägt, über einen vereinbarten Betrag und eine bestimmte Art von Parametern an den Handler übermittelt. Dies ist sehr einfach zu implementieren, aber da verschiedene Ereignisse eine unterschiedliche Anzahl von Parametern haben, müssen einige Workarounds gefunden werden. Die einzige Problemumgehung in diesem Fall wäre, die Ereignisinformationen in eine Struktur (oder eine Klasse) zu packen, die dann an den Handler übergeben wird:

%Vor%

Nun, obwohl das Protokoll zwischen dem Subjekt und seinen Beobachtern einfach ist, ist die tatsächliche Implementierung ziemlich langwierig. Es gibt ein paar Nachteile zu beachten:

Zuerst müssen wir ziemlich viel Code (siehe evSizeChanged ) für jedes Ereignis schreiben. Viel Code ist schlecht.

Zweitens gibt es einige Design-Fragen, die nicht leicht zu beantworten sind: Sollen wir evSizeChanged neben der Klasse Size deklarieren oder neben dem Thema, das sie auslöst? Wenn Sie darüber nachdenken, ist beides nicht ideal. Wird eine Größenänderungsbenachrichtigung immer die gleichen Parameter haben, oder wäre sie abhängig von den einzelnen Themen? (Antwort: Letzteres ist möglich.)

Drittens muss jemand vor dem Abfeuern eine Instanz des Ereignisses erstellen und danach löschen. Also wird der Betreff-Code so aussehen:

%Vor%

Oder wir machen das:

%Vor%

Forth, es gibt ein Casting-Geschäft. Wir haben das Casting innerhalb des Handlers durchgeführt, aber es ist auch möglich, dies in der Fire() -Methode des Subjekts zu tun. Dies beinhaltet jedoch entweder dynamisches Casting (Leistung teuer) oder wir führen eine statische Umwandlung durch, die zu einer Katastrophe führen kann, wenn das Ereignis ausgelöst wird und das Ereignis, das der HF erwartet, nicht übereinstimmt.

Fünftens ist die Handler-Arity wenig lesbar:

%Vor%

Im Gegensatz dazu:

%Vor%

Was uns zum nächsten Abschnitt führt.

Variabilität Drücken

Was den Code betrifft, würden viele Programmierer diesen Code gerne sehen:

%Vor%

Und dieser Beobachtercode:

%Vor%

Die Methoden Fire() des Subjekts und die Observer-Handler unterscheiden sich je Ereignis. Der Code ist lesbar und so kurz, wie wir uns erhofft hätten.

Diese Implementierung beinhaltet einen sehr sauberen Client-Code, würde aber einen ziemlich komplexen Subject -Code (mit einer Vielzahl von Funktionsvorlagen und möglicherweise anderen Goodies) hervorbringen. Dies ist ein Kompromiss, den die meisten Programmierer eingehen werden - es ist besser, komplexen Code an einem Ort zu haben (die Subject-Klasse), als in vielen (dem Client-Code); und angesichts der Tatsache, dass die Klasse des Themas einwandfrei funktioniert, könnte ein Programmierer es einfach als Black-Box betrachten und sich wenig darum kümmern, wie es implementiert wird.

Es lohnt sich, darüber nachzudenken, wie und wann sichergestellt werden soll, dass die Fire -Art und die Handler-Arity übereinstimmen. Wir könnten es in der Laufzeit machen, und wenn die beiden nicht übereinstimmen, erheben wir eine Behauptung. Aber es wäre wirklich schön, wenn wir während der Kompilierzeit einen Fehler bekommen, für den wir die Arity jedes Ereignisses explizit erklären müssen, etwa so:

%Vor%

Wir werden später sehen, wie diese Ereignisdeklaration eine weitere wichtige Rolle spielt.

(Ende von Teil I)

    
Izhaki 31.01.2013, 19:42
quelle
7

(Beginn von Teil II)

Themen

Der Abonnementprozess

Was wird gespeichert?

Abhängig von der spezifischen Implementierung können die Subjekte die folgenden Daten speichern, wenn sich Beobachter anmelden:

  • Ereignis-ID - Das Interesse oder welches Ereignis der Beobachter abonniert hat.
  • Die Observer-Instanz - am häufigsten in Form eines Objektzeigers.
  • Ein Mitgliedsfunktionszeiger - wenn ein beliebiger Handler verwendet wird.

Diese Daten bilden die Parameter der subscribe-Methode:

%Vor%

Es ist erwähnenswert, dass wenn ein beliebiger Handler verwendet wird, Memberfunktionszeiger wahrscheinlich zusammen mit der Observer-Instanz in einer Klasse oder einer Struktur gepackt werden, um einen Delegaten zu bilden. Und so könnte die Methode Subscribe() die folgende Signatur haben:

%Vor%

Das tatsächliche Speichern (möglicherweise in std::map ) beinhaltet die Ereignis-ID als Schlüssel und den Delegaten als Wert.

Implementieren von Ereignis-IDs

Das Definieren von Ereignis-IDs außerhalb der Subjektklasse, die sie auslöst, könnte den Zugriff auf diese IDs vereinfachen. Im Allgemeinen sind die Ereignisse, die von einem Subjekt ausgelöst werden, für dieses Subjekt einzigartig. Daher ist es in den meisten Fällen logisch, die Ereignis-IDs in der Subjektklasse zu deklarieren.

Obwohl es mehrere Möglichkeiten gibt, Ereignis-IDs zu deklarieren, werden hier nur drei besprochen, die am interessantesten sind:

Enums erscheinen auf den ersten Blick am logischsten:

%Vor%

Der Vergleich von enums (was beim Abonnieren und Brennen geschieht) ist schnell. Vielleicht die einzige Schwierigkeit bei dieser Strategie ist, dass Beobachter die Klasse beim Abonnement angeben müssen:

%Vor%

Strings bieten den Enums eine "lockere" Option, da die Subject-Klasse diese normalerweise nicht als Enums deklariert; Stattdessen verwenden Clients einfach:

%Vor%

Das Schöne an Strings ist, dass die meisten Compiler sie anders als andere Parameter farblich codieren, was die Lesbarkeit des Codes irgendwie verbessert:

%Vor%

Aber das Problem mit Strings ist, dass wir zur Laufzeit nicht sagen können, ob wir einen Eventnamen falsch geschrieben haben. Außerdem dauert der Vergleich von Zeichenfolgen länger als der Vergleich von enum, da Zeichenfolgen Zeichen für Zeichen verglichen werden müssen.

Typen ist die letzte hier besprochene Option:

%Vor%

Der Vorteil der Verwendung von Typen ist, dass sie das Überladen von Methoden wie Subscribe() ermöglichen (was wir bald sehen werden, kann ein häufiges Problem mit Beobachtern lösen):

%Vor%

Aber noch einmal, Beobachter brauchen ein wenig zusätzlichen Code zum Abonnieren:

%Vor%

Wo speichern Sie Beobachter?

Implementierungspunkt 1 in Design Pattern beschäftigt sich damit, wo die Beobachter eines jeden Subjekts gespeichert werden sollen. Dieser Abschnitt ergänzt diese Diskussion und bietet 3 Optionen:

  • Globaler Hash
  • Pro Betreff
  • Pro Ereignis

Wie in Design Patterns vorgeschlagen, befindet sich eine Stelle zum Speichern der Subjekt-Beobachter-Map in einer globalen Hash-Tabelle. Die Tabelle enthält das Thema, das Ereignis und den Beobachter (oder Delegierten). Von allen Methoden ist diese Methode am effizientesten, da die Subjekte keine Member-Variable konsumieren, um die Liste der Beobachter zu speichern - es gibt nur eine globale Liste. Dies kann nützlich sein, wenn das Muster in Javascript-Frameworks implementiert wird, da der Speicher von Browsern begrenzt ist. Der Hauptnachteil dieser Methode ist, dass sie auch die langsamste ist - für jedes Ereignis, das abgefeuert wird, müssen wir zuerst das angeforderte Subjekt aus dem globalen Hash filtern, dann das angeforderte Ereignis filtern und erst dann durch alle Beobachter iterieren.

>

In Design Patterns wird auch vorgeschlagen, dass jedes Subjekt eine Liste seiner Beobachter führt. Dies wird etwas mehr Speicher verbrauchen (in Form einer std::map -Membervariable pro Thema), aber es bietet eine bessere Leistung als ein globaler Hash, da das Subjekt nur das angeforderte Ereignis filtern muss und dann alle Beobachter dieses Ereignisses durchlaufen muss . Der Code könnte so aussehen:

%Vor%

In Entwurfsmustern wird nicht die Option vorgeschlagen, jedes Ereignis als Elementvariable zu verwenden und dann Beobachter im Ereignis selbst zu speichern. Dies ist die speicherintensivste Strategie, da nicht nur jedes Ereignis eine Mitgliedsvariable verbraucht, sondern auch ein std::vector , das Beobachter pro Ereignis speichert. Diese Strategie bietet jedoch die beste Leistung, da keine Filterung durchgeführt werden muss und wir einfach durch die angehängten Beobachter iterieren können. Diese Strategie beinhaltet auch den einfachsten Code im Vergleich zu den anderen beiden. Um es zu implementieren, muss eine Veranstaltung Subskriptions- und Brandmethoden anbieten:

%Vor%

Das Thema könnte etwa so aussehen:

%Vor%

Obwohl Beobachter die Ereignisse theoretisch direkt abonnieren könnten, werden wir sehen, dass es sich lohnt, stattdessen das Thema durchzugehen:

%Vor%

Die drei Strategien bieten einen klaren Fall des Kompromisses zwischen Speichern und Vergleichen.Und kann mit der folgenden Tabelle verglichen werden:

Der gewählte Ansatz sollte Folgendes berücksichtigen:

  • Verhältnis von Subjekten / Beobachtern - Die Erinnerungsstrafe wird in Systemen mit wenigen Beobachtern und vielen Subjekten höher sein, besonders wenn das typische Subjekt keinen oder nur einen Beobachter hat.
  • Häufigkeit der Benachrichtigungen - Je häufiger Benachrichtigungen gesendet werden, desto höher ist die Leistungseinbuße.

Wenn das Beobachtermuster verwendet wird, um MouseMove -Ereignisse zu melden, möchte man vielleicht mehr über die Leistung der Implementierung nachdenken. Soweit Speicherabzüge gehen, kann die folgende Berechnung helfen. Gegeben:

  • Verwenden der pro-Ereignis-Strategie
  • Ein typisches 64-Bit-System
  • Jedes Thema hat durchschnittlich 8 Ereignisse

8 Millionen Subjekt-Instanzen verbrauchen knapp 1 GB RAM (nur Ereignisspeicher).

Derselbe Beobachter, dasselbe Ereignis?

Eine Schlüsselfrage bei der Implementierung des Beobachtermusters ist, ob wir es dem gleichen Beobachter erlauben, mehr als einmal dasselbe Ereignis (desselben Themas) zu abonnieren.

Wenn wir das zulassen, werden wir wahrscheinlich std::multimap anstelle von std::map verwenden. Darüber hinaus wird die folgende Zeile problematisch sein:

%Vor%

Da das Subjekt nicht wissen kann, welches der vorherigen Abonnements (es können mehr als eins sein!) abbestellen kann. Daher muss Subscribe() ein Token zurückgeben, das Unsubscribe() verwenden wird, und die gesamte Implementierung wird sehr viel komplexer.

Auf den ersten Blick scheint es ziemlich idiotisch zu sein - warum möchte das gleiche Objekt mehr als einmal dasselbe Ereignis abonnieren? Aber bedenken Sie den folgenden Code:

%Vor%

Dieser spezielle Code führt dazu, dass dasselbe Objekt zweimal zum selben Ereignis abonniert wird. Es ist auch erwähnenswert, dass, da die Methode OnSizeChanged() nicht virtuell ist, der Mitgliedsfunktionszeiger zwischen den beiden Subskriptionsaufrufen unterschiedlich ist. In diesem speziellen Fall könnte das Subjekt also auch den Mitgliedsfunktionszeiger vergleichen, und die Abbestellungssignatur lautet:

%Vor%

Wenn jedoch OnSizeChanged() virtuell ist, gibt es keine Möglichkeit, zwischen den beiden Subskriptionsaufrufen ohne Token zu unterscheiden.

Die Wahrheit ist, wenn OnSizeChanged() virtuell ist, gibt es keinen Grund für die Circle -Klasse, das Ereignis erneut zu abonnieren, da es ein eigener Handler ist, der aufgerufen wird und nicht der der Basisklasse:

%Vor%

Dieser Code stellt wahrscheinlich den besten Kompromiss dar, wenn sowohl die Basisklasse als auch ihre Unterklasse auf dasselbe Ereignis reagieren müssen. Aber es erfordert, dass die Handler virtuell sind und der Programmierer weiß, welche Ereignisse die Basisklasse abonniert.

Wenn derselbe Beobachter nicht mehr als einmal für dasselbe Ereignis registriert wird, wird die Implementierung des Musters erheblich vereinfacht. Es spart die Notwendigkeit, Mitgliedsfunktionszeiger zu vergleichen (ein kniffliges Geschäft) und erlaubt Unsubscribe() , so kurz zu sein (selbst wenn ein MFP mit Subscribe() bereitgestellt wurde):

%Vor%

Konsistenz des Beitragssubskripts

Eines der Hauptziele des Beobachtermusters besteht darin, die Beobachter in Übereinstimmung mit ihrem Subjektstatus zu halten - und wir haben bereits gesehen, dass Zustandsänderungsereignisse genau das tun.

Es ist etwas überraschend, dass es den Autoren von Design Patterns missfiel, zu behaupten, dass wenn ein Beobachter ein Subjekt abonniert, der Zustand des Subjekts noch nicht mit dem Zustand des Subjekts übereinstimmt. Betrachten Sie diesen Code:

%Vor%

Nach der Erstellung subskribiert die Figure -Klasse mit ihrem Subjekt, aber ihre Größe stimmt nicht mit der des Subjekts überein, noch wird die Ansicht aktualisiert, um anzuzeigen, was ihre korrekte Größe haben sollte.

Wenn das Beobachtermuster zum Auslösen eines Zustandsänderungsereignisses verwendet wird, wird es häufig erforderlich sein, die Beobachter nach der Subskription manuell zu aktualisieren. Ein Weg, dies zu erreichen, ist innerhalb des Beobachters:

%Vor%

Stellen Sie sich jedoch ein Thema mit 12 Statusänderungsereignissen vor. Es wäre schön, wenn das Ganze automatisch passieren würde, wobei das Subjekt beim Abonnieren dem Beobachter das richtige Ereignis zurückgibt.

Eine Möglichkeit, dies zu erreichen, erfordert eine überladene Methode Subscribe() im konkreten Thema:

%Vor%

Dann der Beobachtercode:

%Vor%

Beachten Sie, dass der Aufruf Fire nun einen zusätzlichen Parameter ( aDelegate ) benötigt, so dass er nur diesen bestimmten Beobachter und nicht die bereits abonnierten Beobachter aktualisieren kann.

gxObserver behandelt dieses Szenario, indem es gebundene Ereignisse definiert. Dies sind Ereignisse, deren einziger Parameter (mit Ausnahme eines optionalen Senders) an einen Getter oder eine Membervariable gebunden ist:

%Vor%

Dies ermöglicht auch, dass Personen ein Ereignis auslösen, das nur den Ereignistyp bereitstellt:

%Vor%

Unabhängig von dem verwendeten Mechanismus sollte man sich erinnern:

  • Es muss möglich sein, sicherzustellen, dass Beobachter nach dem Abonnement des staatlichen Ereignisses mit ihrem Thema konsistent sind.
  • Es ist besser, wenn dies in der Subjektklasse und nicht im Beobachtercode implementiert ist.
  • Die Fire() -Methode benötigt möglicherweise einen zusätzlichen optionalen Parameter, sodass sie für einen einzelnen Beobachter (den gerade abonnierten) ausgelöst werden kann.

Der Brennvorgang

Feuer von der Basisklasse

Das folgende Code-Snippet zeigt die Implementierung der Ereignisauslösung in JUCE :

%Vor%

Bei diesem Ansatz gibt es einige Probleme:

  • Aus dem Code geht hervor, dass die Klasse ihre eigene Liste von buttonListeners führt, was bedeuten würde, dass sie auch ihre eigenen Methoden AddListener und RemoveListener hat.
  • Das konkrete Thema ist derjenige, der die Liste der Beobachter durchläuft.
  • Das Subjekt ist stark mit seinem Beobachter gekoppelt, da es sowohl seine Klasse ( ButtonListener ) als auch die eigentliche Callback-Methode ( buttonClicked ) kennt.

All diese Punkte bedeuten, dass es keine Basisfachklasse gibt. Wenn dieser Ansatz gewählt wird, muss jeder Auslöse- / Abonnementmechanismus für jedes konkrete Subjekt neu implementiert werden. Dies ist eine gegenobjektorientierte Programmierung.

Es wäre sinnvoll, das Management der Beobachter, ihre Durchsuchung und die eigentliche Benachrichtigung in einer Subjekt-Basisklasse durchzuführen; Auf diese Weise würde jede Änderung des Unterstreichungsmechanismus (z. B. Einführung von Thread-Sicherheit) keine Änderung in jedem konkreten Thema erfordern. Dies wird unsere konkreten Themen mit einer gut gekapselten und einfachen Schnittstelle verlassen und das Feuern wird auf eine Zeile reduziert:

%Vor%

Aussetzung und Wiederaufnahme von Ereignissen

Viele Anwendungen und Frameworks werden die Notwendigkeit finden, die Auslösung von Ereignissen für ein bestimmtes Thema auszusetzen. Manchmal möchten wir, dass die gesperrten Ereignisse anstehen und gefeuert werden, wenn wir weiter schießen, manchmal wollen wir sie einfach ignorieren. Soweit das Thema Schnittstelle geht:

%Vor%

Ein Beispiel für eine sinnvolle Unterbrechung von Ereignissen ist die Zerstörung von zusammengesetzten Objekten. Wenn ein zusammengesetztes Objekt zerstört wird, zerstört es zuerst alle seine Kinder, die zuerst alle ihre Kinder zerstören, und so weiter. Wenn sich diese zusammengesetzten Objekte nun in der Modellschicht befinden, müssen sie ihre entsprechenden Objekte in der Ansichtsebene benachrichtigen (z. B. mit einem evBeforeDestroy -Ereignis):

Nun ist es in diesem speziellen Fall nicht notwendig, dass jedes Objekt ein evBeforeDestroy -Ereignis auslöst - es genügt, wenn nur das Top-Level-Modellobjekt wird (das Löschen des Top-Level-View-Objekts wird auch alle seine Child-Objekte löschen) . Wann immer ein Komposit als solches zerstört wird, möchte es die Ereignisse seiner Kinder unterbrechen (ohne sie in die Warteschlange zu stellen).

Ein anderes Beispiel wäre das Laden eines Dokuments mit vielen Objekten, andere beobachten andere. Während ein Thema zuerst geladen werden kann und seine Größe basierend auf den Dateidaten festgelegt ist, sind seine Beobachter möglicherweise noch nicht geladen und würden daher die Größenänderungsbenachrichtigung nicht erhalten. In diesem Fall möchten wir Ereignisse vor dem Laden anhalten, sie jedoch in die Warteschlange stellen, bis das Dokument vollständig geladen wurde. Wenn alle Ereignisse in der Warteschlange ausgelöst werden, wird sichergestellt, dass alle Beobachter mit ihren Subjekten konsistent sind.

Schließlich wird eine optimierte Warteschlange das gleiche Ereignis für dasselbe Thema nicht mehr als einmal in die Warteschlange stellen. Wenn Benachrichtigungen fortgesetzt werden, besteht kein Grund mehr, Beobachter über eine Größenänderung auf (10, 10) zu informieren, wenn ein später in die Warteschlange eingereihtes Ereignis benachrichtigt wird (20, 20). Daher ist die neueste Version jedes Ereignisses diejenige, die die Warteschlange beibehalten sollte.

Wie kann man einer Klasse Subjektfunktionen hinzufügen?

Ein typisches Subjekt-Interface würde in etwa so aussehen:

%Vor%

Die Frage ist, wie wir diese Schnittstelle verschiedenen Klassen hinzufügen. Es gibt drei Optionen zu beachten:

  • Vererbung
  • Komposition
  • Mehrfachvererbung

Vererbung

In Design Patterns erbt eine ConcreteSubject -Klasse von einer Subject -Klasse.

%Vor%

Sowohl die Klassendiagramme als auch der Beispielcode in Design Pattern können leicht dazu führen, dass man denkt, dass man so vorgeht. Aber das gleiche Buch warnt vor Vererbung und empfiehlt, die Komposition darüber zu bevorzugen. Dies ist sinnvoll: Betrachten Sie eine Anwendung mit vielen Kompositen, in denen nur einige Subjekte sind; Soll die Klasse Composite von der Klasse Subject erben? Wenn das der Fall ist, werden viele Composites über Fähigkeiten verfügen, die sie nicht benötigen, und es kann zu einer Speichereinbuße in Form einer Variablen einer Observer-Liste kommen, die immer leer ist.

Komposition

Die meisten Anwendungen und Frameworks werden die Notwendigkeit finden, die Subjektfunktionen nur in ausgewählte Klassen zu "stecken", bei denen es sich nicht notwendigerweise um Basisklassen handelt. Komposition erlaubt genau das. In der Praxis wird eine Klasse ein Member mSubject haben, das eine Schnittstelle zu allen Subjekt-Methoden bereitstellt, wie zum Beispiel:

%Vor%

Ein Problem mit dieser Strategie besteht darin, dass es für jede von einem Subjekt unterstützte Klasse eine Gedächtnisstrafe (eine Mitgliedsvariable) enthält.Der andere ist, dass es den Zugriff auf das Subjektprotokoll etwas umständlich macht:

%Vor%

Mehrfachvererbung

Die Mehrfachvererbung ermöglicht es uns, das Subjektprotokoll nach Belieben zu einer Klasse zusammenzufassen, aber ohne die Fallstricke der Mitgliederzusammensetzung:

%Vor%

Auf diese Weise werden wir mSubject aus dem vorherigen Beispiel entfernen, so dass uns übrig bleibt:

%Vor%

Beachten Sie, dass wir public virtual für die Subjektvererbung verwenden. Wenn also Unterklassen von ScrollManager das Protokoll erneut erben, erhalten wir die Schnittstelle nicht zweimal. Aber es ist fair anzunehmen, dass Programmierer bemerken werden, dass eine Basisklasse bereits ein Thema ist, daher gibt es keinen Grund, sie wieder zu erben.

Obwohl die Mehrfachvererbung im Allgemeinen nicht gefördert wird und nicht von allen Sprachen unterstützt wird, ist es zu diesem Zweck eine Überlegung wert. ExtJs, das auf Javascript basiert, unterstützt keine Mehrfachvererbung, verwendet Mixins, um dasselbe zu erreichen:

%Vor%

Fazit

Um diesen Artikel abzuschließen, sollten verallgemeinerte Implementierungen des Beobachtermusters diesen Schlüsselpunkten Rechnung tragen:

  • Themen feuern sowohl staatliche als auch staatenlose Ereignisse - letztere können nur mit einem Push-Modell realisiert werden.
  • Themen feuern normalerweise mehr als eine Art von Ereignis .
  • Beobachter können dieselbe Veranstaltung mit einer Vielzahl von Themen abonnieren. Das heißt, das Subjekt-Beobachter-Protokoll sollte es ermöglichen, dass der Absender buchstabiert wird .
  • Arbitrary Event Handler vereinfachen den Client-Code erheblich und erleichtern das bevorzugte Varianz-Push-Modell; aber ihre Implementierung ist nicht geradlinig und wird zu einem komplexeren Thema führen.
  • Event-Handler müssen möglicherweise virtuell sein.
  • Beobachter können in einem globalen Hash pro Thema oder pro Ereignis gespeichert werden. Die Wahl bildet einen Kompromiss zwischen Speicher, Leistung und Einfachheit des Codes.
  • Im Idealfall abonniert ein Beobachter einmal dasselbe Thema für dasselbe Ereignis.
  • Beobachten Sie den Zustand des Beobachters unmittelbar nach dem Abonnieren mit dem Status Ihres Subjekts, denken Sie daran.
  • Das Auslösen von Ereignissen muss möglicherweise mit einer Warteschlangenoption unterbrochen sein und dann wiederaufgenommen sein.
  • Mehrfache Vererbung ist eine Überlegung wert, wenn Betreff-Funktionen zu einer Klasse hinzugefügt werden.

(Ende von Teil II)

    
Izhaki 31.01.2013 19:43
quelle