Design pattern per segnali / eventi bidirezionali

6

Questo problema sembra piuttosto semplice, ma non ho mai conosciuto una soluzione eccezionale. Sto cercando un modo per i componenti di un'applicazione di notificarsi a vicenda mentre sono il più disaccoppiati possibile (sia in fase di compilazione / compilazione sia in fase di esecuzione), ma anche evitando notifiche circolari in modo che i componenti non ho bisogno di auto-mitigare contro. Mi è capitato di riscontrare questo problema (per la centesima volta) in JavaScript al momento, ma questo è incidentale.

Alcuni metodi di disaccoppiamento includono:

  • Iniezione di dipendenza (DI) . In questo caso, utilizzo require.js che consente, ad esempio, di sostituire le implementazioni fittizie nei test unitari creando creazioni alternative require.config() .
  • Invio di eventi . Per esempio. %codice%
  • Pubblica / iscriviti (aka pub / sub).

Gli ultimi due sono fondamentalmente varianti del pattern Observer con diversi pro e contro.

Il problema che voglio risolvere non è affrontato specificamente da questi pattern, e non sono sicuro se si tratta di un dettaglio di implementazione o se c'è un pattern da applicare:

  • fooInstance.listen('action string', barInstance.actionHandler) invia un messaggio (genera un evento, qualunque sia) che la proprietà fooObj è stata modificata
  • "baz" gestisce questo evento chiamando il proprio metodo barObj.bazChanged()
  • setBaz() genera un evento che "baz" è cambiato
  • barObj.setBaz() gestisce questo evento ...

Come caso reale, immagina fooObj.bazChanged() è un componente della GUI, con un cursore per fooObj , e "tempo" è un componente di sequencing musicale che riproduce un punteggio. Il cursore dovrebbe influenzare il tempo del sequencer, ma il punteggio può contenere variazioni di tempo, quindi quando si suona il sequencer si dovrebbe influenzare la posizione del cursore. Una soluzione dovrebbe essere non modale.

Un approccio è aggiungere guardie, ad esempio:

function handleTempoChanged(tempo) {
    if (this.tempo == tempo) return;
    ...
}

Funziona, ma sembra una soluzione scadente perché significa che ogni gestore di eventi ha bisogno di assumere che sia necessaria una protezione, che è brutta norma generale e spesso non richiesta, OPPURE essere a conoscenza degli altri componenti del sistema che renderebbero un ciclo possibile. Probabilmente, questo punto è sbagliato, le guardie dovrebbero sempre essere usate se il gestore sta andando a lanciare un evento modificato direttamente o indirettamente, ma questo sembra ancora una logica di caldaia. Questa potrebbe essere l'unica risposta alla mia domanda ...

Esiste un modello generale per trattare i potenziali cicli come descritto? Si noti che questo è non su sincrono contro asincrono; in entrambi i casi i cicli possono verificarsi.

MODIFICATO per riflettere le opinioni dei commentatori:

Mi rendo conto che è possibile eliminare il boilerplate, usando una sorta di mixin. In pseudo-codice:

class ObservablePropsMixin:

    // generic setter with gaurd
    function set(propName, value):
        if this[propName] = value: return
        this[propName] = value
        _fire(propName, value) 

    // generic method to add listeners
    function observe(propName, handler):
        _observers[propName].add(handler)    

    // private event dispatching method
    function _fire(propName, value):
        foreach observer in _observers[propName]:    
            observer.call(propName, value)

    ...

Questo è semplificato per concentrarsi sulla mia domanda, ma un vero mixin implementerebbe altre semantica di pubsub o eventi o segnali, analoga a qualsiasi implementazione di dispatcher di eventi. I componenti che devono essere osservatori o osservabili dovrebbero ereditare / estendersi dal mixin (quanto sopra presuppone che una certa forma di ereditarietà multipla o aggregata sia possibile nella lingua, che potrebbe essere modificata per funzionare invece con la composizione, ad esempio ObservableMixin.constructor (osservatoObject) piuttosto che usando "questo".

    
posta Jason Boyd 05.03.2014 - 17:37
fonte

4 risposte

2

La soluzione generale per evitare di ripetere il boilerplate è estrarla nella sua stessa classe. In questo caso, di solito creerai una classe Publisher che gestisca l'evitamento dei cicli per te.

var tempoPublisher = new NumericPublisher();

tempoPublisher.onTempoChange(moveTempoSliderWidget);
tempoPublisher.onTempoChange(changeSequencerTempo);

tempoSliderWidget.onSlide(function (percentage) {
    tempoPublisher.publish(percentage * maxTempo);
});

Nel tuo NumericPublisher.publish() metti la guardia. Quindi non importa quanti NumericPublisher s o componenti tu crei, hai solo bisogno della guardia in un unico posto.

Questo rompe anche le dipendenze dei componenti l'uno sull'altro. Devono solo conoscere il tempoPublisher , che può essere iniettato usando DI.

    
risposta data 06.03.2014 - 14:23
fonte
1

Un certo numero di anni fa, ho avuto un problema simile nella progettazione di sistemi in tempo reale. L'approccio generale che ho preso è stato quello di creare un insieme di moduli con ciascun modulo che avesse un numero di faccette: input, output, controllo, manutenzione, repository e un paio di altri. Ogni modulo è stato assegnato a un livello nel sistema - a volte il livello era abbastanza arbitrario. In alcuni casi, un modulo è stato suddiviso in parti, con funzioni di livello inferiore separate da funzioni di livello superiore. In altri casi, le sfaccettature sono state suddivise in livelli (input di livello superiore rispetto agli input di livello inferiore).

Abbiamo stabilito delle regole su come gestire i call-back. Le regole erano basate sulla direzionalità (su / giù, sinistra / destra) e faccette (gli input potevano essere collegati solo alle uscite). Le regole sono state espresse come "questo tipo di marmo può rotolare solo in discesa" o "questa palla può rimbalzare più in alto".

Model-View-Controller ha una serie di regole simili. Inoltre, all'interno dell'ambiente JSP, ci sono regole temporali: alcune osservazioni possono essere fatte solo in determinati momenti. Lo stesso framework MVC impone le regole.

Nel tuo caso, sembra che tu stia cercando di costruire il framework da solo. Pertanto, devi stabilire la sintassi e la semantica del tuo "linguaggio dell'osservatore". Ecco un approccio con cui potresti giocare:

Stabilisci tipi di osservazioni. I tipi si basano sull'impatto dell'osservatore sullo stato del sistema. Stabilisci relazioni tra i tipi.

a) Tipo di visualizzazione (Visualizza in MVC). Il valore osservato viene sempre formattato per la visualizzazione e il valore va solo in una direzione (verso l'utente o verso un report stampato).

b) Tipo di calcolo. Il valore osservato prenderà parte a un calcolo, probabilmente con altri valori osservati, e il risultato può, a sua volta, essere osservato. La circolarità abbonda e ci deve essere un'infrastruttura per identificare la convergenza o la divergenza.

c) Tipo di input. Input dell'utente come campo modulo o discesa. Il risultato va verso l'interno, verso il cuore dell'applicazione. I tipi di Display e Input possono essere composti, ovviamente, ma hanno una relazione di stato che impedisce circoli viziosi.

d) Tipo di validatore. Osserva uno o più valori e suona un campanello quando le cose non vanno bene. Input e Validaters possono essere composti, ancora con relazioni di stato e temporali

...

Stabilendo un protocollo o una lingua o regole d'ingaggio, potresti essere in grado di mantenere la tua sanità mentale. :)

    
risposta data 06.03.2014 - 03:50
fonte
1

Le guardie sono solo una soluzione possibile. Non c'è modo che alcuni codici di terze parti possano distinguere tra il ciclo nella catena di eventi e la normale catena di eventi.

Sebbene le guardie possano sembrare una logica di caldaia, in realtà sono abbastanza semplici da implementare alcuni tipi di loro che possono essere astratti se il linguaggio lo supporta. Ad esempio, onChange potrebbe verificare automaticamente se il valore è cambiato e attivare l'evento solo se il valore cambia.

    
risposta data 06.03.2014 - 15:49
fonte
1

Questa è solo una breve idea. Non l'ho usato o testato personalmente.

Oltre a una guardia basata sul confronto tra il valore corrente e quello nuovo, le notifiche circolari potrebbero essere prevenute con questo schema:

  • Quando un gestore origina una modifica di proprietà, chiede al sistema un ID evento univoco e associa questa modifica di proprietà all'ID evento.
  • Quando un altro gestore riceve questa modifica di proprietà, se ritiene necessario emettere ulteriori modifiche alle proprietà, deve "concatenare" le modifiche alle proprietà aggiuntive con l'ID evento originale, nonché allegare nuovi ID evento a ogni modifica di proprietà aggiuntiva. Pertanto, gli ID evento formeranno un albero, con l'evento originale nella radice.
  • Ogni gestore può memorizzare gli ID evento che ha terminato l'elaborazione e quindi impedire la doppia elaborazione.

In alcuni casi, è possibile ottenere un effetto simile utilizzando un timestamp anziché un ID univoco.

Si noti che questa è un'opzione molto pesante. Non l'ho ancora testato o utilizzato, quindi potrebbe non funzionare come avrei desiderato.

    
risposta data 07.03.2014 - 06:48
fonte