Résumé
Le but de cet article est une tentative de regarder, d'un point de vue différent, une description des systèmes de distribution d'événements.
Au moment d'écrire ces lignes, la plupart des principaux frameworks php implémentent un système d'événements basé sur la description d'un EventObject .
C'est devenu la norme en php, ce qui a été récemment confirmé par l'adoption de la norme psr / event-dispatcher .
Mais il s'avère que la description de l'objet d'événement aide peu à développer l'auditeur. Pour plus de détails sous cat.
Quel est le problème
Examinons les rôles et les objectifs de ceux qui utilisent EventObject dans le développement.
Un développeur (A) qui définit la possibilité d'injecter des instructions tierces dans son processus en générant un événement.
Le développeur décrit l' EventObject ou sa signature via une interface.
Lors de la description d'un EventObject, l'objectif du développeur est de donner aux autres développeurs une description de l'objet de données et, dans certains cas d'utilisation, de décrire le mécanisme d'interaction avec le thread principal via cet objet.
Développeur (B) qui décrit "l'auditeur".
Le développeur s'abonne à l'événement spécifié. Dans la plupart des cas, la description de l'écouteur doit satisfaire le type appelable .
Dans le même temps, le développeur n'hésite pas à nommer les classes ou les méthodes de l'auditeur. Mais il y a une restriction plus par convention que le gestionnaire reçoit un EventObject comme argument.
Lorsque le psr / répartiteur d'événements a été adopté par le groupe de travail, de nombreuses options d'utilisation des systèmes de distribution d'événements ont été analysées.
La norme psr mentionne les cas d'utilisation suivants:
- avis à sens unique - "J'ai fait quelque chose si vous êtes intéressé"
- amélioration d'objet - "Voici une chose, veuillez la changer avant de faire quelque chose avec elle"
- collection - «Donnez-moi toutes vos affaires pour que je puisse faire quelque chose avec cette liste»
- chaîne alternative - "Voici la chose, le premier d'entre vous à y faire face, faites-le, puis arrêtez"
Dans le même temps, le groupe de travail a soulevé de nombreuses questions sur l'utilisation similaire des systèmes de distribution d'événements, liées au fait que chacun des cas d'utilisation a une «incertitude» qui dépend de la mise en œuvre de l'objet Dispatcher .
Dans les rôles décrits ci-dessus pour le développeur (B), il n'existe aucun moyen pratique et bien lisible de comprendre laquelle des options d'utilisation du système d'événements a été choisie par le développeur (A). Le développeur devra toujours examiner le code de description non seulement de l' EventObject , mais également le code où cet événement est généré.
Par conséquent, la signature est une description de l'objet événement, qui est conçue pour faciliter le travail du développeur (B). Ce travail ne fonctionne pas bien.
Un autre problème est la présence pas toujours justifiée d'un objet séparé, qui décrit en plus les entités déjà décrites dans les systèmes.
namespace MyApp\Customer\Events; use MyApp\Customer\CustomerNameInterface; use MyFramevork\Event\SomeEventInterface; class CustomerNameChangedEvent implements SomeEventInterface { public function getCustomerId(): int; public function getCustomerName(): CustomerNameInterface; }
Dans l'exemple ci-dessus, l'objet CustomerNameInterface a déjà été décrit dans le système.
Cela rappelle l'introduction excessive de nouveaux concepts. Après tout, lorsque nous devons implémenter une méthode, par exemple, en écrivant dans le journal des changements de nom du client, nous ne combinons pas les arguments de la méthode dans une entité distincte, nous utilisons la description standard d'une méthode du formulaire:
function writeToLogCustomerNameChange( int $customerId, CustomerNameInterface $customerName ) {
En conséquence, nous voyons les problèmes suivants:
- mauvaise signature du code d'auditeur
- Incertitude du répartiteur
- incertitude du type de retour
- introduction de nombreuses entités supplémentaires telles que SomeEventObject
Regardons les choses sous un angle différent
Si l'un des problèmes est une mauvaise description de l'écouteur, examinons le système de distribution d'événements non pas à partir de la description de l'objet événement, mais à partir de la description de l'écouteur.
Le développeur (A) décrit la manière dont l'auditeur doit être décrit.
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ); }
Un excellent développeur (A) a pu transmettre une description de l'auditeur et des données transmises à des développeurs tiers.
Lors de l'écriture d'un écouteur, le développeur (B) tape CustomerNameChangedListener dans les implémentations d' environnement et l'EDI peut ajouter une description de la méthode d'écoute à sa classe. L'achèvement du code est excellent.
Jetons un coup d'œil à la nouvelle signature de la méthode d'écoute. Un simple coup d'œil suffit pour comprendre que la version du système de distribution d'événements utilisé est: "notification à sens unique".
Les données d'entrée ne sont pas transmises par référence, ce qui signifie qu'il n'y a aucun moyen de les modifier de quelque manière que ce soit pour que les modifications tombent dans le flux principal. Valeur de retour manquante; aucune rétroaction du thread principal.
Qu'en est-il des autres cas d'utilisation? Jouons avec la description de l'interface de l'écouteur d'événements.
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): CustomerNameInterface; }
Il y avait une exigence pour la valeur de retour, ce qui signifie que l'écouteur peut (mais n'est pas obligé de) retourner une valeur différente si elle correspond à l'interface spécifiée. Cas d'utilisation: "amélioration d'objet".
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): array; }
Il y avait une exigence pour la valeur de retour d'un certain type par lequel il peut être compris que c'est un élément de la collection. Cas d'utilisation: "collection".
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): VoteInterface; }
Cas d'utilisation: "chaîne alternative, vote".
namespace MyFramework\Events; interface EventControllInterface { public function stopPropagation(); }
namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName, EventControllInterface &$eventControll ); }
Sans discussion, arrêter la propagation des événements est bon ou mauvais.
Cette option est lue sans ambiguïté, le thread principal fournit à l'auditeur l'occasion d'arrêter l'événement.
Donc, si nous passons à la description de l'écouteur, nous obtenons de meilleures signatures lisibles des méthodes d'écoute lors de leur développement.
De plus, nous avons la possibilité pour le thread principal de pointer explicitement vers:
- acceptabilité des modifications des données entrantes
- types de retour autres que les types entrants
- transfert explicite d'un objet avec lequel arrêter la propagation d'un événement
Comment implémenter un abonnement à un événement
Les options peuvent être différentes. La signification générale de toutes les options se résume au fait que nous devons en quelque sorte informer l'objet ListenerProvider (l'objet qui donne la possibilité de s'abonner à l'événement), auquel événement appartient l'interface spécifique.
Vous pouvez considérer la conversion de l'objet passé en un type appelable comme exemple. Il faut comprendre qu'il peut y avoir de nombreuses options pour obtenir des méta-informations supplémentaires:
- peut être passé explicitement, comme dans l'exemple
- peut être stocké dans des annotations d'interfaces d'écoute
- vous pouvez utiliser le nom de l'interface d'écoute comme nom des événements
Exemple de mise en œuvre de l'abonnement
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'); } } }
Nous ajoutons une méthode de configuration à l'objet d'abonnement qui, pour chaque interface d'écoute, décrit ses métadonnées, telles que la méthode appelée et le nom de l'événement.
Selon ces données, au moment de l'abonnement, nous transformons le gestionnaire $ passé en un objet appelable indiquant la méthode appelée.
Si vous remarquez, le code implique qu'un objet $ handler peut implémenter de nombreuses interfaces d'écouteur d'événements et sera abonné à chacune d'entre elles. Il s'agit d'un analogue de SubscriberInterface pour l'abonnement en masse d'un objet à plusieurs événements. Comme vous pouvez le voir, l'implémentation ci-dessus ne nécessite pas de mécanisme séparé comme addSubscriber(SubscriberInterface $subscriber)
il s'est avéré fonctionner hors de la boîte.
Dispatcher
Hélas, l'approche décrite va à l'encontre de l'interface acceptée comme la norme psr / event-dispatcher
Puisque nous n'avons pas besoin de passer d'objet à Dispatcher. Oui, vous pouvez passer un objet comme le sucre:
class Event { public function __construct(string $eventName, ...$arguments) {
Et utilisez-le lors de la génération d'un événement sur l'interface psr, mais c'est tout simplement laid.
Dans le bon sens, l'interface Dispatcher ressemblerait mieux à ceci:
interface EventDispatcherInterface { public function dispatch(string $eventName, ...$arguments); public function dispatchStopabled(string $eventName, ...$arguments); }
Pourquoi deux méthodes? Il est difficile de combiner tous les cas d'utilisation en une seule implémentation. Il est préférable d'ajouter votre propre méthode pour chaque cas d'utilisation, il y aura une interprétation sans ambiguïté de la façon dont Dispatcher traitera les valeurs renvoyées par les écouteurs.
C’est tout. Il serait intéressant de discuter avec la communauté si l'approche décrite a droit à la vie.