Come facilitare la manutenzione del codice guidato dagli eventi?

14

Quando si utilizza un componente basato su eventi, spesso provo dolore durante la fase di manutenzione.

Poiché il codice eseguito è tutto diviso, può essere piuttosto difficile capire quale sarà la parte di codice che sarà coinvolta in fase di esecuzione.

Questo può portare a problemi delicati e difficili da 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 e gestori di applicazioni che delegano le attività ad altre parti dell'app, c'è un momento in cui il codice inizia a diventare difficile da leggere perché ci sono molti gestori registrati da molti posti diversi (particolarmente vero quando c'è un autobus).

Quindi il diagramma di sequenza inizia a guardare oltre il tempo speso complesso per capire cosa sta succedendo è in aumento e la sessione di debug diventa disordinata (punto di interruzione sul gestore gestori mentre iterazione su gestori, particolarmente gioioso con gestore asincrono e alcuni filtri su 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 e per evitare l'accoppiamento tra diversi componenti, vengono attivati alcuni eventi: uno prima che la query venga inviata, uno quando la risposta viene restituita e un altro in caso di errore. Disponiamo di un set di 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 query, registri, analisi dati, filtraggio dati, massaggio dati, animazione fantasia UI, catena più query sequenziali, qualunque cosa). Quindi alcuni gestori devono essere eseguiti prima / dopo alcuni altri e sono registrati da un sacco di diversi punti di ingresso nell'applicazione.

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

Questo progetto è emerso perché l'uso dell'ereditarietà stava iniziando a essere un disastro completo. Il sistema degli eventi viene utilizzato in un tipo di composizione in cui non si sa ancora quali saranno i tuoi compositi.

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

Quindi mi chiedo in che modo gli altri stanno affrontando questo tipo di codice. Sia quando lo scrivi sia quando lo leggi.

Hai qualche metodo o strumento che ti permette di scrivere e mantenere tale codice senza troppi dolori?

    
posta Guillaume 16.07.2012 - 15:14
fonte

7 risposte

6

Ho scoperto che elaborare eventi utilizzando una serie di eventi interni (in particolare, una coda LIFO con rimozione arbitraria) semplifica enormemente la programmazione basata sugli eventi. Permette di dividere l'elaborazione di un "evento esterno" in diversi "eventi interni" più piccoli, con uno stato ben definito intermedio. Per ulteriori informazioni, vedere la mia risposta a questa domanda .

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

Supponiamo che tu stia usando l'oggetto A per eseguire alcuni servizi, e tu gli dai una richiamata per informarti quando è fatto. Tuttavia, A è tale che dopo aver chiamato il callback, potrebbe essere necessario fare un po 'più di lavoro. 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 ti viene chiamato da A - se A, dopo il ritorno della richiamata, non riesce a capire che è stato distrutto, potrebbe verificarsi un arresto anomalo quando tenta di eseguire il lavoro rimanente.

NOTA: È vero che si potrebbe fare la "distruzione" in qualche altro modo, come decrementare un refcount, ma che porta solo a stati intermedi, e codice extra e bug dalla gestione di questi; meglio per A smettere semplicemente di funzionare interamente dopo che non ne hai più bisogno se non continuare in qualche stato intermedio.

Nel mio pattern, A farebbe semplicemente pianificare il lavoro ulteriore che deve fare spingendo un evento interno (job) nella coda LIFO del ciclo degli eventi, quindi procedere a richiamare il callback e torna immediatamente al ciclo degli eventi . Questo pezzo di codice non è più un azzardo, dal momento che A ritorna. Ora, se il callback non distrugge A, il lavoro spinto verrà infine eseguito dal loop degli eventi per eseguire il proprio lavoro extra (dopo la callback e tutti i relativi lavori, in modo ricorsivo). D'altra parte, se il callback distrugge A, la funzione di destructor o deinit di A può rimuovere il lavoro in push dallo stack di eventi, impedendo implicitamente l'esecuzione del lavoro in push.

    
risposta data 29.07.2012 - 11:16
fonte
7

Penso che la registrazione corretta possa essere di grande aiuto. Assicurati che ogni evento lanciato / gestito sia registrato da qualche parte (puoi usare i framework di registrazione per questo). Quando esegui il debug, puoi consultare i log per vedere l'ordine di esecuzione esatto del tuo codice quando si è verificato il bug. Spesso questo aiuterà davvero a restringere la causa del problema.

    
risposta data 16.07.2012 - 15:55
fonte
3

So I'm wondering how other people are tackling this kind of code. Both when writing and reading it.

Il modello della programmazione basata sugli eventi semplifica la codifica in una certa misura. Probabilmente si è evoluto in sostituzione di grandi istruzioni Select (o case) utilizzate in lingue più vecchie e guadagnato popolarità nei primi ambienti di sviluppo visivi come VB 3 (non citatemi nella cronologia, non l'ho verificato)!

Il modello diventa un dolore se la sequenza di eventi è importante e quando 1 azione commerciale viene suddivisa tra molti eventi. Questo stile di processo viola i benefici di questo approccio. Ad ogni costo, provare a rendere il codice di azione incapsulato nell'evento corrispondente e non generare eventi dagli eventi. Che poi diventa molto peggio degli Spaghetti derivanti dal GoTo.

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

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

    
risposta data 16.07.2012 - 17:43
fonte
3

Volevo aggiornare questa risposta da quando ho avuto alcuni momenti di eureka dopo aver "appiattito" e "appiattito" i flussi di controllo e ho formulato alcune nuove idee sull'argomento.

Effetti collaterali complessi contro flussi di controllo complessi

Quello che ho scoperto è che il mio cervello può tollerare complessi effetti di controllo o di controllo grafico come di solito si trovano nella gestione degli eventi, ma non nella combo 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 ciclo sequenziale for . Il mio cervello può tollerare un ciclo sequenziale che ridimensiona e riposiziona elementi, li anima, li ridisegna e aggiorna un qualche tipo di stato ausiliario. È abbastanza facile da comprendere.

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

Dove mi perdo, 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 succederà di conseguenza e in quale ordine. Quindi è la combinazione di queste due cose che lo rende così scomodo dove, anche se il codice funziona perfettamente bene in questo momento, è così spaventoso cambiarlo senza timore di causare effetti collaterali indesiderati.

I flussi di controllo complessi tendono a rendere difficile ragionare su quando / dove le cose stanno per accadere. Questo diventa veramente causa di mal di testa se questi complessi flussi di controllo innescano 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 dall'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 descritto che è così difficile da comprendere? La strategia è semplificare il flusso di controllo o gli effetti collaterali.

Una strategia ampiamente applicabile per semplificare gli effetti collaterali è favorire l'elaborazione differita. Come esempio, la normale tentazione potrebbe essere quella di riapplicare il layout della GUI, riposizionare e ridimensionare i widget secondari, attivare un'altra cascata di applicazioni di layout e ridimensionare e riposizionare la gerarchia, oltre a ridipingere i controlli, probabilmente attivando alcuni eventi unici per i widget che hanno un comportamento di ridimensionamento personalizzato che innesca più eventi che portano a chi sa dove, ecc. Invece di provare a farlo tutto in un solo passaggio o inviando spam alla coda degli eventi, una possibile soluzione è scendere la gerarchia dei widget e indica quali widget necessitano di aggiornare i loro layout. Quindi, in un passaggio differito successivo che ha un flusso di controllo sequenziale diretto, riapplicare tutti i layout per i widget che ne hanno bisogno. Potresti quindi contrassegnare quali widget devono essere ridipinti. Sempre in un passaggio differito sequenziale con un flusso di controllo semplice, ridisegnare i widget contrassegnati come bisognosi di essere ridisegnati.

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

Ciò comporta un sovraccarico di elaborazione, ma potrebbe quindi aprire le porte, ad esempio, eseguendo questi passaggi posticipati in parallelo, consentendo potenzialmente di ottenere una soluzione ancora più efficiente di quella che hai avviato se le prestazioni sono un problema. Generalmente la prestazione non dovrebbe essere molto preoccupante nella maggior parte dei casi però. Ancora più importante, anche se questo potrebbe sembrare una differenza contraria, ho trovato molto più facile ragionare. Rende molto più facile prevedere cosa sta accadendo e quando, e non posso sopravvalutare il valore che può avere nel riuscire a capire più facilmente cosa sta succedendo.

    
risposta data 15.01.2016 - 04:16
fonte
2

Ciò che ha funzionato per me è rendere ogni evento indipendente, senza riferimento ad altri eventi. Se stanno arrivando in modo asincrono, non devi avere una sequenza, quindi cercare di capire cosa succede in quale ordine è inutile, oltre ad essere impossibile.

Ciò che si finisce con sono un mucchio di strutture dati che vengono letti e modificati e creati e rimossi da una dozzina di thread senza un ordine particolare. Devi fare una programmazione multi-threaded, che non è facile. Devi anche pensare al multi-thread, come in "Con questo evento, guarderò i dati che ho in un determinato istante, senza riguardo a ciò che era un microsecondo prima, senza riguardo a ciò che lo ha appena cambiato e senza riguardo a ciò che i 100 thread che mi aspettano per rilasciare il blocco lo faranno, poi farò i miei cambiamenti basandomi su questo e su quello che vedo, quindi ho finito. "

Una cosa che mi trovo a fare è scansionare una raccolta particolare e assicurarmi che sia il riferimento che la raccolta stessa (se non di sicurezza) siano bloccati correttamente e sincronizzati correttamente con altri dati. Con l'aggiunta di altri eventi, questo compito cresce. Ma se stavo seguendo le relazioni tra gli eventi, il lavoro di che sarebbe cresciuto molto più velocemente. Inoltre a volte un sacco di blocchi può essere isolato con il proprio metodo, rendendo 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. Due volte il numero di eventi richiede solo il doppio del lavoro e forse solo 1,5 volte tanto. Cercare di coordinare eventi più asincroni ti seppellirà rapidamente.

    
risposta data 16.07.2012 - 18:15
fonte
2

Sembra che si sta cercando Stato Macchine e amp; Attività guidate dagli eventi .

Tuttavia, si potrebbe anche voler guardare macchina Stato Markup Workflow campione.

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

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

    
risposta data 16.07.2012 - 15:55
fonte
1

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

Ciò che è veramente difficile da debug sono le chiamate alle funzioni generate dinamicamente. Il modello (anti?) Che chiamerei la fabbrica del richiamo dall'inferno. Tuttavia, questo tipo di fabbriche di funzioni è altrettanto difficile da eseguire il debug nel flusso tradizionale.

    
risposta data 16.07.2012 - 16:08
fonte

Leggi altre domande sui tag