Comment faciliter la maintenance du code événementiel?

16

Lorsque j'utilise un composant basé sur les événements, je ressens souvent une certaine douleur lors de la phase de maintenance.

Étant donné que le code exécuté est entièrement divisé, il peut être assez difficile de déterminer quelle sera la partie du code qui sera impliquée lors de l'exécution.

Cela peut entraîner des problèmes subtils et difficiles à déboguer lorsque quelqu'un ajoute de nouveaux gestionnaires d'événements.

Modifier à partir des commentaires: même avec certaines bonnes pratiques à bord, comme avoir un bus d'événements à l'échelle de l'application et des gestionnaires déléguant des affaires à une autre partie de l'application, il y a un moment où le code commence à devenir difficile à lire car il y a beaucoup de gestionnaires enregistrés de nombreux endroits différents (particulièrement vrai quand il y a un bus).

Ensuite, le diagramme de séquence commence à sembler complexe, le temps passé à comprendre ce qui se passe augmente et la session de débogage devient compliquée (point d'arrêt sur le gestionnaire de gestionnaires tout en itérant sur les gestionnaires, particulièrement joyeux avec le gestionnaire asynchrone et certains filtrages par-dessus).

////////////////
Exemple

J'ai un service qui récupère des données sur le serveur. Sur le client, nous avons un composant de base qui appelle ce service à l'aide d'un rappel. Pour fournir un point d'extension aux utilisateurs du composant et éviter le couplage entre différents composants, nous déclenchons certains événements: un avant l'envoi de la requête, un lorsque la réponse revient et un autre en cas d'échec. Nous avons un ensemble de base de gestionnaires pré-enregistrés qui fournissent le comportement par défaut du composant.

Maintenant, les utilisateurs du composant (et nous sommes également utilisateurs du composant) peuvent ajouter des gestionnaires pour effectuer des changements sur le comportement (modifier la requête, les journaux, l'analyse des données, le filtrage des données, le massage des données, l'animation de fantaisie de l'interface utilisateur, la chaîne de plusieurs requêtes séquentielles , peu importe). Certains gestionnaires doivent donc être exécutés avant / après d'autres et ils sont enregistrés à partir de nombreux points d'entrée différents dans l'application.

Après un certain temps, il peut arriver qu'une douzaine ou plus de gestionnaires soient enregistrés, et travailler avec cela peut être fastidieux et dangereux.

Cette conception a émergé parce que l'utilisation de l'héritage commençait à être un gâchis complet. Le système d'événements est utilisé dans une sorte de composition où vous ne savez pas encore quels seront vos composites.

Fin de l'exemple
////////////////

Je me demande donc comment d'autres personnes s'attaquent à ce type de code. À la fois lors de l'écriture et de la lecture.

Avez-vous des méthodes ou des outils qui vous permettent d'écrire et de maintenir un tel code sans trop de peine?

Guillaume
la source
Vous voulez dire, en plus de refactoriser la logique des gestionnaires d'événements?
Telastyn
Documentez ce qui se passe.
@Telastyn, je ne suis pas sûr de bien comprendre ce que vous entendez par «en plus de refactoriser la logique des gestionnaires d'événements».
Guillaume
@Thorbjoern: voir ma mise à jour.
Guillaume
3
Il semble que vous n'utilisez pas le bon outil pour le travail? Je veux dire, si l'ordonnance est importante, alors vous ne devriez pas utiliser des événements simples pour ces parties de la demande en premier lieu. Fondamentalement, s'il existe des limitations sur l'ordre des événements dans un système de bus typique, ce n'est plus un système de bus typique, mais vous l'utilisez toujours en tant que tel. Ce qui signifie que vous devez résoudre ce problème en ajoutant une logique supplémentaire pour gérer la commande. Au moins, si je comprends bien votre problème ..
stijn

Réponses:

7

J'ai constaté que le traitement des événements à l'aide d'une pile d'événements internes (plus précisément, une file d'attente LIFO avec suppression arbitraire) simplifie considérablement la programmation pilotée par les événements. Il vous permet de diviser le traitement d'un "événement externe" en plusieurs "événements internes" plus petits, avec un état bien défini entre les deux. Pour plus d'informations, voir ma réponse à cette question .

Ici, je présente un exemple simple qui est résolu par ce modèle.

Supposons que vous utilisez l'objet A pour effectuer un service et que vous lui rappeliez pour vous informer quand c'est fait. Cependant, A est tel qu'après avoir appelé votre rappel, il peut être nécessaire d'effectuer un travail supplémentaire. Un danger survient lorsque, dans ce rappel, vous décidez que vous n'avez plus besoin de A et que vous le détruisez d'une manière ou d'une autre. Mais vous êtes appelé depuis A - si A, après le retour de votre rappel, ne peut pas comprendre en toute sécurité qu'il a été détruit, un crash peut se produire lorsqu'il tente d'effectuer le travail restant.

REMARQUE: il est vrai que vous pouvez effectuer la "destruction" d'une autre manière, comme décrémenter un refcount, mais cela ne fait que conduire à des états intermédiaires et à du code et à des bogues supplémentaires pour les gérer; mieux pour A d'arrêter de fonctionner complètement après que vous n'en ayez plus besoin, sinon de continuer dans un état intermédiaire.

Dans mon modèle, A planifierait simplement le travail supplémentaire qu'il doit faire en poussant un événement interne (travail) dans la file d'attente LIFO de la boucle d'événements, puis procéderait à l'appel du rappel et reviendrait immédiatement à la boucle d'événements . Ce morceau de code n'est plus un danger, car A revient. Maintenant, si le rappel ne détruit pas A, le travail poussé sera finalement exécuté par la boucle d'événement pour faire son travail supplémentaire (une fois le rappel terminé et tous ses travaux poussés, récursivement). D'un autre côté, si le rappel détruit A, le destructeur ou la fonction deinit de A peut supprimer le travail poussé de la pile d'événements, empêchant implicitement l'exécution du travail poussé.

Ambroz Bizjak
la source
7

Je pense qu'une bonne journalisation peut être très utile. Assurez-vous que chaque événement lancé / géré est enregistré quelque part (vous pouvez utiliser des cadres de journalisation pour cela). Lorsque vous déboguez, vous pouvez consulter les journaux pour voir l' ordre exact d'exécution de votre code lorsque le bogue s'est produit. Souvent, cela aidera vraiment à réduire la cause du problème.

Oleksi
la source
Oui, c'est un conseil utile. La quantité de journaux produits peut alors être effrayante (je me souviens avoir eu du mal à trouver la cause d'un cycle dans le traitement événementiel d'une forme très complexe.)
Guillaume
Une solution consiste peut-être à consigner les informations intéressantes dans un fichier journal distinct. De plus, vous pouvez attribuer un UUID à chaque événement, afin de pouvoir suivre chaque événement plus facilement. Vous pouvez également écrire un outil de rapport qui vous permet d'extraire des informations spécifiques des fichiers journaux. (Ou bien, utilisez des commutateurs dans le code pour enregistrer différentes informations dans différents fichiers journaux).
Giorgio
3

Je me demande donc comment d'autres personnes s'attaquent à ce type de code. À la fois lors de l'écriture et de la lecture.

Le modèle de programmation événementielle simplifie le codage dans une certaine mesure. Il a probablement évolué en remplacement des grandes déclarations Select (ou case) utilisées dans les langages plus anciens et a gagné en popularité dans les premiers environnements de développement visuel tels que VB 3 (ne me citez pas sur l'historique, je ne l'ai pas vérifié)!

Le modèle devient pénible si la séquence d'événements est importante et lorsqu'une action commerciale est répartie sur plusieurs événements. Ce style de processus viole les avantages de cette approche. À tout prix, essayez de faire encapsuler le code d'action dans l'événement correspondant et ne déclenchez pas d'événements à partir d'événements. Cela devient alors bien pire que les Spaghetti issus du GoTo.

Parfois, les développeurs sont désireux de fournir des fonctionnalités GUI qui nécessitent une telle dépendance aux événements, mais il n'y a vraiment pas de véritable alternative beaucoup plus simple.

L'essentiel ici est que la technique n'est pas mauvaise si elle est utilisée à bon escient.

Aucune chance
la source
Leur conception est-elle alternative pour éviter le «pire que les spaghettis»?
Guillaume
Je ne suis pas sûr qu'il existe un moyen facile sans simplifier le comportement attendu de l'interface graphique, mais si vous répertoriez toutes vos interactions utilisateur avec une fenêtre / page et pour chacune déterminez exactement ce que fera votre programme, vous pouvez commencer à regrouper le code en un seul endroit et diriger plusieurs événements à un groupe de sous-méthodes / méthodes qui sont responsables du traitement réel de la demande. Il peut également être utile de séparer le code qui ne sert que l'interface graphique du code qui effectue le traitement principal.
NoChance
Le besoin de publier ce message est venu d'une refactorisation lorsque je suis passé d'un enfer d'héritage à quelque chose basé sur l'événement. À un moment donné, c'est vraiment mieux, mais à certains autres, c'est assez mauvais ... Étant donné que j'ai déjà eu des problèmes avec les `` événements déchaînés '' dans certaines interfaces graphiques, je me demande ce qui peut être fait pour améliorer la maintenance de tels code de tranche d'événement.
Guillaume
La programmation événementielle est bien plus ancienne que VB; il était présent dans l'interface graphique de SunTools, et avant cela, je semble me souvenir qu'il a été intégré au langage Simula.
kevin cline
@Guillaume, je suppose que vous êtes sur l'ingénierie du service. Ma description ci-dessus était en fait basée principalement sur les événements GUI. Avez-vous (vraiment) besoin de ce type de traitement?
NoChance
3

Je voulais mettre à jour cette réponse car j'ai eu quelques instants eureka depuis après les flux de contrôle "aplatissant" et "aplatissant" et j'ai formulé de nouvelles réflexions sur le sujet.

Effets secondaires complexes vs flux de contrôle complexes

Ce que j'ai trouvé, c'est que mon cerveau peut tolérer des effets secondaires complexes ou des flux de contrôle de type graphique complexes, comme vous le voyez généralement avec la gestion des événements, mais pas la combinaison des deux.

Je peux facilement raisonner sur le code qui provoque 4 effets secondaires différents s'ils sont appliqués avec un flux de contrôle très simple, comme celui d'une forboucle séquentielle . Mon cerveau peut tolérer une boucle séquentielle qui redimensionne et repositionne les éléments, les anime, les redessine et met à jour une sorte de statut auxiliaire. C'est assez facile à comprendre.

Je peux également comprendre un flux de contrôle complexe que vous pourriez obtenir avec des événements en cascade ou traversant une structure de données de type graphique complexe s'il n'y a qu'un effet secondaire très simple en cours dans le processus où l'ordre n'a pas la moindre importance, comme le marquage d'éléments à traiter de façon différée dans une simple boucle séquentielle.

Là où je me perds, je suis confus et submergé, c'est lorsque vous avez des flux de contrôle complexes provoquant des effets secondaires complexes. Dans ce cas, le flux de contrôle complexe rend difficile de prédire à l'avance où vous allez vous retrouver tandis que les effets secondaires complexes rendent difficile de prédire exactement ce qui va se passer en conséquence et dans quel ordre. C'est donc la combinaison de ces deux choses qui le rend si mal à l'aise où, même si le code fonctionne parfaitement bien en ce moment, il est si effrayant de le changer sans craindre de provoquer des effets secondaires indésirables.

Les flux de contrôle complexes ont tendance à rendre difficile de raisonner quand / où les choses vont se passer. Cela ne devient vraiment un casse-tête que si ces flux de contrôle complexes déclenchent une combinaison complexe d'effets secondaires où il est important de comprendre quand / où les choses se produisent, comme les effets secondaires qui ont une sorte de dépendance à l'ordre où une chose devrait se produire avant la suivante.

Simplifiez le flux de contrôle ou les effets secondaires

Alors, que faites-vous lorsque vous rencontrez le scénario ci-dessus qui est si difficile à comprendre? La stratégie consiste à simplifier le flux de contrôle ou les effets secondaires.

Une stratégie largement applicable pour simplifier les effets secondaires consiste à favoriser le traitement différé. En utilisant un événement de redimensionnement de l'interface graphique à titre d'exemple, la tentation normale peut être de réappliquer la disposition de l'interface graphique, de repositionner et de redimensionner les widgets enfants, de déclencher une autre cascade d'applications de mise en page et de redimensionner et de repositionner la hiérarchie, ainsi que de repeindre les contrôles, en déclenchant éventuellement certains des événements uniques pour les widgets qui ont un comportement de redimensionnement personnalisé qui déclenche plus d'événements conduisant à qui sait où, etc. et marquez quels widgets ont besoin que leurs mises en page soient mises à jour. Ensuite, dans une passe différée ultérieure qui a un flux de contrôle séquentiel simple, réappliquez toutes les dispositions pour les widgets qui en ont besoin. Vous pourriez alors marquer quels widgets doivent être repeints. Encore une fois dans une passe différée séquentielle avec un flux de contrôle simple, repeignez les widgets marqués comme devant être redessinés.

Cela a pour effet à la fois de simplifier le flux de contrôle et les effets secondaires car le flux de contrôle devient simplifié car il ne cascade pas les événements récursifs pendant la traversée du graphe. Au lieu de cela, les cascades se produisent dans la boucle séquentielle différée qui pourrait ensuite être gérée dans une autre boucle séquentielle différée. Les effets secondaires deviennent simples là où cela compte, car pendant les flux de contrôle de type graphique plus complexes, nous ne faisons que marquer ce qui doit être traité par les boucles séquentielles différées qui déclenchent les effets secondaires plus complexes.

Cela s'accompagne de frais généraux de traitement, mais cela pourrait alors ouvrir des portes pour, par exemple, effectuer ces passes différées en parallèle, ce qui pourrait vous permettre d'obtenir une solution encore plus efficace que vous avez commencé si les performances sont un problème. En règle générale, les performances ne devraient cependant pas être une préoccupation majeure dans la plupart des cas. Plus important encore, bien que cela puisse sembler une différence théorique, je l'ai trouvé beaucoup plus facile à raisonner. Il est beaucoup plus facile de prédire ce qui se passe et quand, et je ne peux pas surestimer la valeur que peut avoir de pouvoir comprendre plus facilement ce qui se passe.


la source
2

Ce qui a fonctionné pour moi, c'est que chaque événement se démarque de lui-même, sans référence à d'autres événements. Si elles arrivent asynchroniously, vous n'avez une séquence, donc à essayer de comprendre ce qui se passe dans quel ordre est inutile, en plus d' être impossible.

Vous vous retrouvez avec un tas de structures de données qui sont lues, modifiées, créées et supprimées par une douzaine de threads sans ordre particulier. Vous devez faire une programmation multi-thread extrêmement appropriée, ce qui n'est pas facile. Vous devez également penser à plusieurs threads, comme dans "Avec cet événement, je vais regarder les données que j'ai à un instant particulier, sans tenir compte de ce que c'était une microseconde plus tôt, sans tenir compte de ce qui vient de le changer , et sans tenir compte de ce que les 100 threads qui attendent que je libère le verrou vont y faire. Ensuite, je ferai mes modifications en fonction de cela même et de ce que je vois. Ensuite, j'ai terminé. "

Une chose que je me trouve en train de scanner pour une collection particulière et de m'assurer que la référence et la collection elle-même (sinon threadsafe) sont verrouillées correctement et synchronisées correctement avec d'autres données. À mesure que de nouveaux événements sont ajoutés, cette corvée grandit. Mais si je suivais les relations entre les événements, cette corvée augmenterait beaucoup plus rapidement. De plus, une grande partie du verrouillage peut parfois être isolée dans sa propre méthode, ce qui rend le code plus simple.

Traiter chaque thread en tant qu'entité complètement indépendante est difficile (en raison du multi-thread hard-core) mais réalisable. «Évolutif» est peut-être le mot que je recherche. Deux fois plus d'événements ne prennent que deux fois plus de travail, et peut-être seulement 1,5 fois plus. Essayer de coordonner plus d'événements asynchrones vous enterrera rapidement.

RalphChapin
la source
J'ai mis à jour ma question. Vous avez tout à fait raison sur les séquences et les éléments asynchrones. Comment géreriez-vous un cas où l'événement X doit être exécuté après le traitement de tous les autres événements? Il semble que je doive créer un gestionnaire de gestionnaires plus complexe, mais je veux également éviter d'exposer trop de complexité aux utilisateurs.
Guillaume
Dans votre ensemble de données, disposez d'un commutateur et de certains champs qui sont définis lorsque l'événement X est lu. Lorsque tous les autres sont traités, vous vérifiez le commutateur et savez que vous devez gérer X et que vous avez les données. Le commutateur et les données devraient être autonomes, en fait. Une fois réglé, vous devriez penser: «Je dois faire ce travail», pas «Je dois suspendre X.» Prochain problème: comment savez-vous que les événements se déroulent? et si vous obtenez 2 événements X ou plus? Dans le pire des cas, vous pouvez exécuter un thread de maintenance en boucle qui vérifie la situation et peut agir de sa propre initiative. (Aucune entrée pendant 3 secondes? Commutateur X réglé? Ensuite, exécutez le code d'arrêt.
RalphChapin
2

Il semble que vous recherchiez des machines d'état et des activités pilotées par les événements .

Cependant, vous pouvez également consulter l'exemple de flux de travail de balisage de la machine d'état .

Voici un bref aperçu de la mise en œuvre de la machine d'état. Un workflow de machine d'état se compose d'états. Chaque état est composé d'un ou plusieurs gestionnaires d'événements. Chaque gestionnaire d'événements doit contenir un délai ou un IEventActivity comme première activité. Chaque gestionnaire d'événements peut également contenir une activité SetStateActivity qui est utilisée pour passer d'un état à un autre.

Chaque workflow de machine d'état a deux propriétés: InitialStateName et CompletedStateName. Lorsqu'une instance du flux de travail de la machine d'état est créée, elle est placée dans la propriété InitialStateName. Lorsque la machine d'état atteint la propriété CompletedStateName, elle termine l'exécution.

Yusubov
la source
2
Bien que cela puisse théoriquement répondre à la question, il serait préférable d'inclure ici les parties essentielles de la réponse et de fournir le lien de référence.
Thomas Owens
Mais si vous avez des dizaines de gestionnaires attachés à chaque état, vous ne serez pas si bien quand vous essayez de comprendre ce qui se passe ... Et chaque composant basé sur un événement peut ne pas être décrit comme une machine d'état.
Guillaume
1

Le code événementiel n'est pas le vrai problème. En fait, je n'ai aucun problème à suivre la logique dans le code piloté même, où le rappel est explicitement défini ou des rappels en ligne sont utilisés. Par exemple , les rappels de style générateur dans Tornado sont très faciles à suivre.

Ce qui est vraiment difficile à déboguer, ce sont les appels de fonction générés dynamiquement. Le motif (anti?) Que j'appellerais l'usine de rappel de l'enfer. Cependant, ce type d'usines fonctionnelles est tout aussi difficile à déboguer dans le flux traditionnel.

vartec
la source