Come facilitare la manutenzione del codice guidato dagli eventi?


16

Quando utilizzo un componente basato su eventi, sento spesso dolore durante la fase di mantenimento.

Dal momento che il codice eseguito è tutto suddiviso, può essere abbastanza difficile capire quale sarà tutta la parte di codice che sarà coinvolta in fase di esecuzione.

Questo può portare a problemi sottili e difficili da eseguire il debug quando qualcuno aggiunge alcuni nuovi gestori di eventi.

Modifica dai commenti: anche con alcune buone pratiche a bordo, come avere un bus di eventi a livello di applicazione e gestori che delegano affari ad altre parti dell'app, c'è un momento in cui il codice inizia a diventare difficile da leggere perché c'è un sacco di gestori registrati da molti luoghi diversi (specialmente vero quando c'è un autobus).

Quindi il diagramma di sequenza inizia a guardare complesso, il tempo impiegato per capire cosa sta accadendo sta aumentando e la sessione di debug diventa disordinata (breakpoint sul gestore dei gestori mentre itera su gestori, particolarmente gioioso con il gestore asincrono e alcuni filtri sopra di esso).

//////////////
Esempio

Ho un servizio che sta recuperando alcuni dati sul server. Sul client abbiamo un componente di base che chiama questo servizio utilizzando un callback. Per fornire un punto di estensione agli utenti del componente ed evitare l'accoppiamento tra componenti diversi, stiamo generando alcuni eventi: uno prima dell'invio della query, uno quando la risposta ritorna e un altro in caso di errore. Abbiamo un set base di gestori preregistrati che forniscono il comportamento predefinito del componente.

Ora gli utenti del componente (e anche noi siamo utenti del componente) possono aggiungere alcuni gestori per apportare alcune modifiche al comportamento (modificare la query, i registri, l'analisi dei dati, il filtro dei dati, il massaggio dei dati, l'animazione fantasia dell'interfaccia utente, la catena di query sequenziali multiple , qualunque cosa). Quindi alcuni gestori devono essere eseguiti prima / dopo altri e sono registrati da molti punti di ingresso diversi nell'applicazione.

Dopo un po ', può succedere che una dozzina o più di gestori siano registrati e lavorare con questo può essere noioso e pericoloso.

Questo progetto è emerso perché l'utilizzo dell'ereditarietà stava iniziando a diventare un casino completo. Il sistema di eventi viene utilizzato in una sorta di composizione in cui non sai ancora quali saranno i tuoi compositi.

Fine dell'esempio
//////////////

Quindi mi chiedo come le altre persone stanno affrontando questo tipo di codice. Sia durante la scrittura che la lettura.

Hai qualche metodo o strumento che ti permetta di scrivere e mantenere tale codice senza troppa sofferenza?


Intendi, oltre al refactoring della logica dai gestori di eventi?
Telastyn,

Documenta cosa succede.

@Telastyn, non sono sicuro di capire appieno cosa intendi con 'oltre a refactoring della logica dai gestori di eventi'.
Guillaume,

@Thorbjoern: vedi il mio aggiornamento.
Guillaume,

3
Sembra che tu non stia utilizzando lo strumento giusto per il lavoro? Voglio dire, se l'ordine conta, allora non dovresti usare eventi semplici per quelle parti dell'applicazione in primo luogo. Fondamentalmente se ci sono limitazioni sull'ordine degli eventi in un tipico sistema di bus, non è più un tipico sistema di bus, ma lo stai ancora usando come tale. Ciò significa che è necessario risolvere il problema aggiungendo una logica aggiuntiva per gestire l'ordine. Almeno, se capisco correttamente il tuo problema ..
stijn,

Risposte:


7

Ho scoperto che l'elaborazione di eventi utilizzando una pila di eventi interni (più specificamente, una coda LIFO con rimozione arbitraria) semplifica notevolmente la programmazione guidata dagli eventi. Consente di dividere l'elaborazione di un "evento esterno" in diversi "eventi interni" più piccoli, con uno stato ben definito tra. Per ulteriori informazioni, vedere la mia risposta a questa domanda .

Qui presento un semplice esempio che è risolto da questo modello.

Supponiamo di utilizzare l'oggetto A per eseguire alcuni servizi e di richiamarlo per informarti quando è terminato. Tuttavia, A è tale che dopo aver richiamato il callback, potrebbe essere necessario svolgere qualche lavoro in più. Un pericolo sorge quando, all'interno di quel callback, decidi che non hai più bisogno di A e lo distruggi in un modo o nell'altro. Ma vieni chiamato da A - se A, dopo il ritorno della richiamata, non riesce a capire con sicurezza che è stato distrutto, potrebbe verificarsi un arresto anomalo quando tenta di eseguire il lavoro rimanente.

NOTA: è vero che potresti fare la "distruzione" in qualche altro modo, come decrementare un refcount, ma questo porta solo a stati intermedi, e codice aggiuntivo e bug dalla gestione di questi; meglio che A smetta di funzionare completamente dopo che non ne hai più bisogno se non continuare in qualche stato intermedio.

Nel mio modello, A avrebbe semplicemente programmato l'ulteriore lavoro che deve fare spingendo un evento interno (lavoro) nella coda LIFO del ciclo di eventi, quindi procederà a chiamare il callback e tornerà immediatamente al ciclo di eventi . Questo pezzo di codice non è più un pericolo, poiché A ritorna. Ora, se il callback non distrugge A, il lavoro push alla fine verrà eseguito dal loop degli eventi per fare il suo lavoro extra (dopo che il callback è stato fatto, e tutti i suoi lavori push, in modo ricorsivo). D'altra parte, se il callback distrugge A, la funzione di distruttore o deinit di A può rimuovere il lavoro push dallo stack di eventi, impedendo implicitamente l'esecuzione del lavoro push.


7

Penso che un'adeguata registrazione possa essere di grande aiuto. Assicurarsi che ogni evento generato / gestito sia registrato da qualche parte (è possibile utilizzare i framework di registrazione per questo). Durante il debug, puoi consultare i log per vedere l' ordine esatto di esecuzione del codice quando si è verificato il bug. Spesso questo aiuterà davvero a restringere la causa del problema.


Sì, è un consiglio utile. La quantità di tronchi prodotti può quindi essere spaventosa (ricordo di aver avuto qualche difficoltà a trovare la causa di un ciclo nell'elaborazione dell'evento di una forma molto complessa).
Guillaume,

Forse una soluzione è quella di registrare le informazioni interessanti in un file di registro separato. Inoltre, puoi assegnare un UUID a ciascun evento, in modo da poter tracciare ogni evento più facilmente. È inoltre possibile scrivere alcuni strumenti di reporting che consentono di estrarre informazioni specifiche dai file di registro. (O in alternativa, utilizzare gli interruttori nel codice per registrare informazioni diverse in file di registro diversi).
Giorgio,

3

Quindi mi chiedo come le altre persone stanno affrontando questo tipo di codice. Sia durante la scrittura che la lettura.

Il modello di programmazione basata sugli eventi semplifica in qualche modo la codifica. Probabilmente si è evoluto come una sostituzione delle grandi dichiarazioni Select (o case) utilizzate nelle lingue più vecchie e ha guadagnato popolarità nei primi ambienti di sviluppo visivo come VB 3 (non citarmi nella storia, non l'ho verificato)!

Il modello diventa un problema se la sequenza di eventi è importante e quando 1 azione aziendale è suddivisa in più eventi. Questo stile di processo viola i vantaggi di questo approccio. A tutti i costi, prova a incapsulare il codice di azione nell'evento corrispondente e non generare eventi all'interno degli eventi. Che poi diventa molto peggio degli Spaghetti risultanti dal GoTo.

A volte gli sviluppatori sono ansiosi di fornire funzionalità GUI che richiedono tale dipendenza da eventi, ma in realtà non esiste alcuna alternativa reale che sia significativamente più semplice.

La linea di fondo qui è che la tecnica non è male se usata con saggezza.


C'è un loro design alternativo per evitare il "peggio degli spaghetti"?
Guillaume,

Non sono sicuro che esista un modo semplice senza semplificare il comportamento della GUI previsto, ma se elenchi tutte le interazioni dell'utente con una finestra / pagina e per ognuna determini esattamente cosa farà il tuo programma, potresti iniziare a raggruppare il codice in un posto e dirigere numerosi eventi a un gruppo di sottotitoli / metodi responsabili dell'elaborazione effettiva della richiesta. Anche la separazione del codice che serve solo la GUI dal codice che esegue l'elaborazione back-end può aiutare.
NoChance,

La necessità di pubblicare questo messaggio è nata da un refactoring quando sono passato da un inferno ereditario a qualcosa basato sull'evento. Su un certo punto è davvero meglio, ma su un altro è piuttosto male ... Dal momento che ho già avuto qualche problema con "eventi impazziti" in alcune GUI, mi chiedo cosa si possa fare per migliorare la manutenzione di tali codice suddiviso per eventi.
Guillaume,

La programmazione basata su eventi è molto più antica di VB; era presente nella GUI di SunTools e prima di allora mi sembra di ricordare che è stato incorporato nel linguaggio Simula.
Kevin Cline,

@Guillaume, immagino tu stia progettando troppo il servizio. La mia descrizione di cui sopra era in realtà basata principalmente sugli eventi della GUI. Hai (davvero) bisogno di questo tipo di elaborazione?
NoChance,

3

Volevo aggiornare questa risposta da quando ho ottenuto alcuni momenti eureka da dopo i flussi di controllo "appiattimento" e "appiattimento" e ho formulato alcuni nuovi pensieri sull'argomento.

Effetti collaterali complessi vs. Flussi di controllo complessi

Quello che ho scoperto è che il mio cervello può tollerare effetti collaterali complessi o complessi complessi flussi di controllo simili a quelli di un grafico, come si trova in genere con la gestione degli eventi, ma non la combinazione di entrambi.

Posso facilmente ragionare sul codice che causa 4 diversi effetti collaterali se vengono applicati con un flusso di controllo molto semplice, come quello di un sequenziale for loop . Il mio cervello può tollerare un ciclo sequenziale che ridimensiona e riposiziona gli elementi, li anima, li ridisegna e aggiorna una sorta di stato ausiliario. È abbastanza facile da capire.

Posso anche comprendere un flusso di controllo complesso come si potrebbe ottenere con eventi a cascata o attraversando una struttura di dati complessa simile a un grafico se c'è un effetto collaterale molto semplice in corso nel processo in cui l'ordine non conta il minimo bit, come contrassegnare gli elementi da elaborare in modo differito in un semplice ciclo sequenziale.

Il punto in cui mi perdo, sono confuso e sopraffatto è quando si hanno flussi di controllo complessi che causano effetti collaterali complessi. In tal caso, il complesso flusso di controllo rende difficile prevedere in anticipo dove finirai, mentre i complessi effetti collaterali rendono difficile prevedere esattamente cosa accadrà di conseguenza e in quale ordine. Quindi è la combinazione di queste due cose che lo rende così scomodo in cui, anche se il codice funziona perfettamente ora, è così spaventoso cambiarlo senza paura di causare effetti collaterali indesiderati.

Flussi di controllo complessi tendono a rendere difficile ragionare su quando / dove stanno andando le cose. Ciò diventa davvero inducente al mal di testa se questi complessi flussi di controllo stanno innescando una complessa combinazione di effetti collaterali in cui è importante capire quando / dove accadono le cose, come gli effetti collaterali che hanno una sorta di dipendenza dell'ordine in cui una cosa dovrebbe accadere prima della successiva.

Semplifica il flusso di controllo o gli effetti collaterali

Quindi cosa fai quando incontri lo scenario sopra che è così difficile da capire? La strategia è di semplificare il flusso di controllo o gli effetti collaterali.

Una strategia ampiamente applicabile per semplificare gli effetti collaterali è favorire l'elaborazione differita. Utilizzando un evento di ridimensionamento della GUI come esempio, la normale tentazione potrebbe essere quella di riapplicare il layout della GUI, riposizionare e ridimensionare i widget figlio, innescare un'altra cascata di applicazioni di layout e ridimensionare e riposizionare verso il basso nella gerarchia, insieme a ridipingere i controlli, eventualmente attivando eventi unici per widget con comportamento di ridimensionamento personalizzato che attivano più eventi che portano a chissà dove, ecc. Invece di provare a farlo tutto in un passaggio o inviando spam alla coda degli eventi, una possibile soluzione è scendere la gerarchia dei widget e segna quali widget richiedono l'aggiornamento dei loro layout. Quindi, in un passaggio posticipato successivo che ha un flusso di controllo sequenziale semplice, riapplica tutti i layout per i widget che ne hanno bisogno. È quindi possibile contrassegnare quali widget devono essere ridipinti. Sempre in un passaggio posticipato sequenziale con un flusso di controllo semplice, ridipingere i widget contrassegnati come da ridisegnare.

Ciò ha l'effetto sia di semplificare il flusso di controllo sia degli effetti collaterali, poiché il flusso di controllo diventa semplificato poiché non si verificano eventi ricorsivi in ​​cascata durante l'attraversamento del grafico. Invece le cascate si verificano nel loop sequenziale differito che potrebbe quindi essere gestito in un altro loop sequenziale differito. Gli effetti collaterali diventano semplici dove conta poiché, durante i più complessi flussi di controllo simili a grafici, tutto ciò che stiamo facendo è semplicemente contrassegnare ciò che deve essere elaborato dai loop sequenziali differiti che attivano gli effetti collaterali più complessi.

Ciò comporta un certo sovraccarico di elaborazione, ma potrebbe quindi aprire le porte a, diciamo, fare questi passaggi differiti in parallelo, potenzialmente permettendoti di ottenere una soluzione ancora più efficiente di quella che hai iniziato se le prestazioni sono un problema. In generale, tuttavia, le prestazioni non dovrebbero essere fonte di preoccupazione. Soprattutto, mentre questa potrebbe sembrare una differenza controversa, ho trovato molto più facile ragionare. Rende molto più facile prevedere cosa sta succedendo e quando, e non posso sopravvalutare il valore che può avere nel poter comprendere più facilmente cosa sta succedendo.


2

Ciò che ha funzionato per me è far sì che ogni evento sia autonomo, senza riferimento ad altri eventi. Se arrivano in modo asincrono, non hai una sequenza, quindi cercare di capire cosa succede in quale ordine è inutile, oltre ad essere impossibile.

Quello che fai alla fine sono un mucchio di strutture di dati che vengono lette, modificate, create e rimosse da una dozzina di thread in nessun ordine particolare. Devi fare una programmazione multi-thread estremamente appropriata, il che non è facile. Devi anche pensare a più thread, come in "Con questo evento, esaminerò i dati che ho in un determinato istante, indipendentemente da quello che era un microsecondo prima, indipendentemente da ciò che lo ha appena cambiato , e indipendentemente da ciò che i 100 thread in attesa che io rilasci il lucchetto lo faranno. Poi apporterò le mie modifiche in base a questo e ciò che vedo. Poi ho finito. "

Una cosa che mi ritrovo a fare è cercare una particolare Collezione e assicurarmi che sia il riferimento che la raccolta stessa (se non thread-safe) siano bloccati correttamente e sincronizzati correttamente con altri dati. Man mano che vengono aggiunti più eventi, questa faccenda cresce. Ma se seguissi le relazioni tra gli eventi, quel lavoro sarebbe cresciuto molto più velocemente. Inoltre a volte gran parte del blocco può essere isolato con il proprio metodo, rendendo in realtà il codice più semplice.

Trattare ogni thread come un'entità completamente indipendente è difficile (a causa del multi-threading hard-core) ma fattibile. "Scalabile" potrebbe essere la parola che sto cercando. Il doppio di eventi richiede solo il doppio del lavoro e forse solo 1,5 volte di più. Cercare di coordinare eventi più asincroni ti seppellirà rapidamente.


Ho aggiornato la mia domanda. Hai perfettamente ragione su sequenze e cose asincrone. Come gestiresti un caso in cui l'evento X deve essere eseguito dopo che tutti gli altri eventi sono stati elaborati? Sembra che debba creare un gestore Handlers più complesso, ma voglio anche evitare di esporre troppa complessità agli utenti.
Guillaume,

Nel tuo set di dati, hai un interruttore e alcuni campi che sono impostati quando viene letto l'evento X. Quando tutti gli altri vengono elaborati, controlli l'interruttore e sai che devi gestire X e hai i dati. L'interruttore e i dati dovrebbero essere autonomi, in realtà. Quando impostato, dovresti pensare "Devo fare questo lavoro", non "Devo gestire X." Prossimo problema: come fai a sapere che gli eventi sono stati fatti? e se ricevi 2 o più eventi X? Nel peggiore dei casi, è possibile eseguire un thread di manutenzione in loop che controlla la situazione e può agire di propria iniziativa. (Nessun input per 3 secondi? Interruttore X impostato? Quindi eseguire il codice di spegnimento.
RalphChapin

2

Sembra che tu stia cercando macchine statali e attività guidate da eventi .

Tuttavia, è possibile che si desideri esaminare anche l' esempio del flusso di lavoro di markup di State Machine .

Ecco una breve panoramica dell'implementazione della macchina a stati. Un flusso di lavoro della macchina a stati è costituito da stati. Ogni stato è composto da uno o più gestori di eventi. Ogni gestore eventi deve contenere un ritardo o un IEventActivity come prima attività. Ogni gestore di eventi può contenere anche un'attività SetStateActivity utilizzata per passare da uno stato a un altro.

Ogni flusso di lavoro della macchina a stati ha due proprietà: InitialStateName e CompletedStateName. Quando viene creata un'istanza del flusso di lavoro della macchina a stati, viene inserita nella proprietà InitialStateName. Quando la macchina a stati raggiunge la proprietà CompletedStateName, termina l'esecuzione.


2
Sebbene ciò possa teoricamente rispondere alla domanda, sarebbe preferibile includere qui le parti essenziali della risposta e fornire il collegamento come riferimento.
Thomas Owens

Ma se hai dozzine di gestori collegati a ogni stato, non starai così bene quando provi a capire cosa sta succedendo ... E ogni componente basato su eventi potrebbe non essere descritto come una macchina a stati.
Guillaume,

1

Il codice guidato dagli eventi non è il vero problema. In effetti, non ho alcun problema a seguire la logica anche nel codice guidato, in cui i callback sono definiti esplicitamente o vengono utilizzati callback in linea. Ad esempio i callback in stile generatore in Tornado sono molto facili da seguire.

Ciò che è veramente difficile da eseguire il debug sono chiamate di funzione generate dinamicamente. Il modello (anti?) Che definirei Call-back Factory dall'Inferno. Tuttavia, questo tipo di fabbriche di funzioni sono ugualmente difficili da eseguire il debug nel flusso tradizionale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.