Ein alternativer Ansatz zum Abonnieren von Ereignissen, oder ist EventObject wirklich notwendig?

Zusammenfassung


Der Zweck dieses Artikels ist der Versuch, eine Beschreibung von Ereignisverteilungssystemen aus einem anderen Blickwinkel zu betrachten.

Zum Zeitpunkt dieses Schreibens implementieren die meisten führenden PHP- Frameworks ein Ereignissystem, das auf einer Beschreibung eines EventObject basiert .

Dies ist zum Standard in PHP geworden, was kürzlich durch die Übernahme des PSR / Event-Dispatcher- Standards bestätigt wurde.

Es stellt sich jedoch heraus, dass die Beschreibung des Ereignisobjekts wenig zur Entwicklung des Listeners beiträgt. Für Details unter Kat.


Was ist das Problem


Schauen wir uns die Rollen und Ziele derer an, die EventObject in der Entwicklung verwenden.


  1. Ein Entwickler (A), der die Möglichkeit festlegt, Anweisungen von Drittanbietern durch Generieren eines Ereignisses in seinen Prozess einzufügen.


    Der Entwickler beschreibt das EventObject oder seine Signatur über eine Schnittstelle.


    Bei der Beschreibung von EventObject ist es das Ziel des Entwicklers, anderen Entwicklern eine Beschreibung des Datenobjekts zu geben und in einigen Anwendungsfällen den Mechanismus der Interaktion mit dem Hauptthread über dieses Objekt zu beschreiben.


  2. Entwickler (B), der den "Listener" beschreibt.


    Der Entwickler abonniert das angegebene Ereignis. In den meisten Fällen sollte die Listener-Beschreibung dem aufrufbaren Typ entsprechen.


    Gleichzeitig scheut der Entwickler nicht, Klassen oder Methoden des Listeners zu benennen. Es gibt jedoch eher eine Einschränkung, dass der Handler ein EventObject als Argument empfängt.



Als der psr / event-dispatcher von der Arbeitsgruppe übernommen wurde, wurden viele Optionen für die Verwendung von Ereignisverteilungssystemen analysiert.


Der psr-Standard erwähnt die folgenden Anwendungsfälle:


  1. One-Way-Hinweis - "Ich habe etwas getan, wenn Sie interessiert sind"
  2. Objektverbesserung - "Hier ist eine Sache, bitte ändern Sie sie, bevor ich etwas damit mache"
  3. Sammlung - "Gib mir all deine Sachen, damit ich mit dieser Liste etwas anfangen kann"
  4. alternative Kette - "Hier ist das Ding, der erste von euch, der damit fertig wird, es tut und dann aufhört"

Gleichzeitig warf die Arbeitsgruppe viele Fragen zur ähnlichen Verwendung von Ereignisverteilungssystemen auf, die damit zusammenhängen, dass jeder Anwendungsfall eine „Unsicherheit“ aufweist, die von der Implementierung des Dispatcher- Objekts abhängt.


In den oben für Entwickler (B) beschriebenen Rollen gibt es keine bequeme und gut lesbare Möglichkeit zu verstehen, welche der Optionen für die Verwendung des Ereignissystems von Entwickler (A) ausgewählt wurde. Der Entwickler muss immer den Beschreibungscode nicht nur des EventObjects , sondern auch des Codes, in dem dieses Ereignis generiert wird, überprüfen .


Infolgedessen ist die Signatur eine Beschreibung des Ereignisobjekts, die die Arbeit des Entwicklers erleichtern soll (B). Dieser Job ist nicht gut.


Ein weiteres Problem ist das nicht immer gerechtfertigte Vorhandensein eines separaten Objekts, das zusätzlich die bereits in den Systemen beschriebenen Entitäten beschreibt.


namespace MyApp\Customer\Events; use MyApp\Customer\CustomerNameInterface; use MyFramevork\Event\SomeEventInterface; class CustomerNameChangedEvent implements SomeEventInterface { /** *     * @return int */ public function getCustomerId(): int; /** *    */ public function getCustomerName(): CustomerNameInterface; } 

Im obigen Beispiel wurde das CustomerNameInterface- Objekt bereits im System beschrieben.


Dies erinnert an die übermäßige Einführung neuer Konzepte. Wenn wir beispielsweise eine Methode implementieren müssen, indem wir in das Namensänderungsprotokoll des Clients schreiben, kombinieren wir die Methodenargumente nicht in einer separaten Entität, sondern verwenden die Standardbeschreibung einer Methode des Formulars:


  function writeToLogCustomerNameChange( int $customerId, CustomerNameInterface $customerName ) { // ... } 

Infolgedessen sehen wir die folgenden Probleme:


  1. schlechte Listener-Code-Signatur
  2. Unsicherheit des Dispatchers
  3. Rückgabetypunsicherheit
  4. Einführung vieler zusätzlicher Entitäten wie SomeEventObject

Betrachten wir es aus einer anderen Perspektive


Wenn eines der Probleme eine schlechte Beschreibung des Listeners ist, betrachten wir das Ereignisverteilungssystem nicht anhand der Beschreibung des Ereignisobjekts, sondern anhand der Beschreibung des Listeners.


Entwickler (A) beschreibt, wie der Listener beschrieben werden soll.


  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ); } 

Der ausgezeichnete Entwickler (A) konnte eine Beschreibung des Hörers und der übertragenen Daten an Drittentwickler weitergeben.


Beim Schreiben eines Listeners gibt der Entwickler (B) CustomerNameChangedListener in die Implementierungsumgebung ein, und die IDE kann seiner Klasse eine Beschreibung der Listener-Methode hinzufügen. Die Code-Vervollständigung ist großartig.


Werfen wir einen Blick auf die neue Signatur der Listener-Methode. Schon ein kurzer Blick genügt, um zu verstehen, dass die Version des verwendeten Ereignisverteilungssystems "Einwegbenachrichtigung" lautet.


Die Eingabedaten werden nicht als Referenz übertragen, was bedeutet, dass es keine Möglichkeit gibt, sie in irgendeiner Weise so zu ändern, dass die Änderungen in den Hauptstrom fallen. Fehlender Rückgabewert, keine Rückmeldung vom Haupt-Thread.


Was ist mit anderen Anwendungsfällen? Spielen wir mit der Beschreibung der Ereignis-Listener-Oberfläche.




  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): CustomerNameInterface; } 

Es gab eine Anforderung für den Rückgabewert, was bedeutet, dass der Listener einen anderen Wert zurückgeben kann (aber nicht muss), wenn er mit der angegebenen Schnittstelle übereinstimmt. Anwendungsfall: "Objektverbesserung".




  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { /** * @return ItemInterface[] */ public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): array; } 

Es gab eine Anforderung für den Rückgabewert eines bestimmten Typs, anhand derer verstanden werden kann, dass dies ein Element der Sammlung ist. Anwendungsfall: "Sammlung".




  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): VoteInterface; } 

Anwendungsfall: "Alternative Kette, Abstimmung."




  namespace MyFramework\Events; interface EventControllInterface { public function stopPropagation(); } 

  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName, EventControllInterface &$eventControll ); } 

Ohne Diskussion ist es gut oder schlecht, die Ausbreitung von Ereignissen zu stoppen.


Diese Option wird eindeutig gelesen. Der Hauptthread bietet dem Listener die Möglichkeit, das Ereignis zu stoppen.


Wenn wir also zur Beschreibung des Listeners übergehen, erhalten wir bei der Entwicklung viel besser lesbare Signaturen der Listener-Methoden.


Zusätzlich haben wir die Möglichkeit, auf den Haupt-Thread explizit zu verweisen auf:


  1. Akzeptanz von Änderungen eingehender Daten
  2. Rückgabetypen außer eingehenden Typen
  3. explizite Übertragung eines Objekts, mit dem die Ausbreitung eines Ereignisses gestoppt werden soll

So implementieren Sie ein Ereignisabonnement


Optionen können unterschiedlich sein. Die allgemeine Bedeutung aller Optionen beruht auf der Tatsache, dass wir das ListenerProvider- Objekt (das Objekt, das die Möglichkeit bietet, das Ereignis zu abonnieren) irgendwie darüber informieren müssen, zu welchem ​​Ereignis die spezifische Schnittstelle gehört.


Sie können die Konvertierung des übergebenen Objekts in einen aufrufbaren Typ als Beispiel betrachten. Es versteht sich, dass es viele Möglichkeiten geben kann, zusätzliche Metainformationen zu erhalten:


  1. kann wie im Beispiel explizit übergeben werden
  2. kann in Anmerkungen von Listener-Schnittstellen gespeichert werden
  3. Sie können den Namen der Listener-Oberfläche als Namen der Ereignisse verwenden



Beispiel für die Implementierung eines Abonnements


  namespace MyFramework\Events; class ListenerProvider { private $handlerAssociation = []; public function addHandlerAssociation( string $handlerInterfaceName, string $handlerMethodName, string $eventName ) { $this->handlerAssociation[$handlerInterfaceName] = [ 'methodName' => $handlerMethodName, 'eventName' => $eventName ]; } public function addHandler(object $handler) { $hasAssociation = false; foreach( $this->handlerAssociation as $handlerInterfaceName => $handlerMetaData ) { if ( $handler interfaceof $handlerInterfaceName ) { $methodName = $handlerMetaData['methodName']; $eventName = $handlerMetaData['eventName']; $this->addListener($eventName, [$handler, $methodName]); $hasAssociation = true; } } if ( !$hasAssociation ) { throw new \Exception('Unknown handler object'); } } } 

Wir fügen dem Abonnementobjekt eine Konfigurationsmethode hinzu, die für jede Listener-Schnittstelle ihre Metadaten beschreibt, z. B. die aufgerufene Methode und den Namen des Ereignisses.


Nach diesen Daten wandeln wir zum Zeitpunkt des Abonnements den übergebenen $ -Handler in ein aufrufbares Objekt um, das die aufgerufene Methode angibt.


Wenn Sie bemerken, impliziert der Code, dass ein $ handler-Objekt viele Ereignis-Listener-Schnittstellen implementieren kann und für jede von ihnen abonniert wird. Dies ist ein Analogon von SubscriberInterface für das Massenabonnieren eines Objekts für mehrere Ereignisse. Wie Sie sehen können, ist in der obigen Implementierung kein separater Mechanismus erforderlich, da addSubscriber(SubscriberInterface $subscriber) funktioniert.


Dispatcher


Leider läuft der beschriebene Ansatz der als psr / event-dispatcher-Standard akzeptierten Schnittstelle zuwider


Da müssen wir kein Objekt an Dispatcher übergeben. Ja, Sie können ein Objekt wie Zucker übergeben:


  class Event { public function __construct(string $eventName, ...$arguments) { // ... } public function getEventName(): string { // ... } public function getArguments(): array { // ... } } 

Und verwenden Sie es, wenn Sie ein Ereignis auf der psr-Schnittstelle generieren, aber es ist einfach nur hässlich.


In guter Weise würde die Dispatcher-Oberfläche folgendermaßen besser aussehen:


  interface EventDispatcherInterface { public function dispatch(string $eventName, ...$arguments); public function dispatchStopabled(string $eventName, ...$arguments); } 

Warum zwei Methoden? Es ist schwierig, alle Anwendungsfälle in einer einzigen Implementierung zu kombinieren. Es ist besser, für jeden Anwendungsfall eine eigene Methode hinzuzufügen. Es wird eindeutig interpretiert, wie der Dispatcher die von den Listenern zurückgegebenen Werte verarbeitet.


Das ist alles Es wäre interessant, mit der Community zu diskutieren, ob der beschriebene Ansatz das Recht auf Leben hat.

Source: https://habr.com/ru/post/de463313/


All Articles