Perché i soggetti non sono consigliati in .NET Reactive Extensions?


111

Attualmente sto prendendo confidenza con il framework Reactive Extensions per .NET e sto lavorando a modo mio attraverso le varie risorse introduttive che ho trovato (principalmente http://www.introtorx.com )

La nostra applicazione coinvolge una serie di interfacce hardware che rilevano i frame di rete, questi saranno i miei IObservables, quindi ho una varietà di componenti che consumeranno quei frame o eseguiranno un qualche modo di trasformazione sui dati e produrranno un nuovo tipo di frame. Ci saranno anche altri componenti che devono visualizzare, ad esempio, ogni ennesimo fotogramma. Sono convinto che Rx sarà utile per la nostra applicazione, tuttavia sto lottando con i dettagli di implementazione per l'interfaccia IObserver.

La maggior parte (se non tutte) delle risorse che ho letto hanno detto che non dovrei implementare l'interfaccia IObservable da solo, ma utilizzare una delle funzioni o classi fornite. Dalla mia ricerca sembra che la creazione di un Subject<IBaseFrame>mi fornisca ciò di cui ho bisogno, avrei il mio singolo thread che legge i dati dall'interfaccia hardware e quindi chiama la funzione OnNext della mia Subject<IBaseFrame>istanza. I diversi componenti di IObserver riceveranno quindi le loro notifiche da quell'oggetto.

La mia confusione viene dal consiglio dato nell'appendice di questo tutorial dove dice:

Evita l'uso dei tipi di soggetto. Rx è effettivamente un paradigma di programmazione funzionale. Usare i soggetti significa che ora stiamo gestendo lo stato, che è potenzialmente in mutamento. Affrontare contemporaneamente lo stato mutante e la programmazione asincrona è molto difficile da ottenere. Inoltre, molti degli operatori (metodi di estensione) sono stati scritti con cura per garantire il mantenimento di una durata corretta e coerente di abbonamenti e sequenze; quando introduci argomenti, puoi romperlo. Le versioni future potrebbero anche vedere una riduzione significativa delle prestazioni se si utilizzano esplicitamente i soggetti.

La mia applicazione è piuttosto critica per le prestazioni, ovviamente testerò le prestazioni dell'utilizzo dei pattern Rx prima che entri nel codice di produzione; tuttavia sono preoccupato di fare qualcosa che è contro lo spirito del framework Rx utilizzando la classe Subject e che una versione futura del framework possa danneggiare le prestazioni.

C'è un modo migliore per fare quello che voglio? Il thread di polling hardware verrà eseguito continuamente indipendentemente dal fatto che ci siano osservatori o meno (altrimenti il ​​buffer HW eseguirà il backup), quindi questa è una sequenza molto calda. Devo quindi trasmettere i frame ricevuti a più osservatori.

Qualsiasi consiglio sarebbe molto apprezzato.


1
Ha davvero aiutato la mia comprensione dell'argomento, mi sto solo mettendo le cose in testa su come usarlo nella mia applicazione. So che sono la cosa giusta: ho una pipeline di componenti che sono molto orientati al push e ho bisogno di fare tutti i tipi di filtri e invocazioni sul thread dell'interfaccia utente per visualizzarli in una GUI, nonché il buffering dell'ultimo frame ricevuto ecc. ecc - Devo solo assicurarmi di farlo bene la prima volta!
Anthony

Risposte:


70

Ok, se ignoriamo i miei modi dogmatici e ignoriamo "i soggetti sono buoni / cattivi" tutti insieme. Esaminiamo lo spazio problema.

Scommetto che hai uno o due stili di sistema a cui devi ingraziarti.

  1. Il sistema genera un evento o una richiamata quando arriva un messaggio
  2. È necessario eseguire il polling del sistema per vedere se ci sono messaggi da elaborare

Per l'opzione 1, facile, lo avvolgiamo semplicemente con il metodo FromEvent appropriato e il gioco è fatto. Al Pub!

Per l'opzione 2, ora dobbiamo considerare come sondare questo e come farlo in modo efficiente. Anche quando otteniamo il valore, come lo pubblichiamo?

Immagino che tu voglia un thread dedicato per il polling. Non vorresti che qualche altro programmatore martellasse ThreadPool / TaskPool e ti lasci in una situazione di fame di ThreadPool. In alternativa, non vuoi il fastidio del cambio di contesto (immagino). Quindi supponiamo di avere il nostro thread, probabilmente avremo una sorta di ciclo While / Sleep in cui ci sediamo per interrogare. Quando il controllo trova alcuni messaggi li pubblichiamo. Ebbene, tutto questo sembra perfetto per Observable.Create. Ora probabilmente non possiamo usare un ciclo While in quanto ciò non ci consentirà di restituire un Disposable per consentire la cancellazione. Fortunatamente hai letto l'intero libro, quindi sei esperto con la pianificazione ricorsiva!

Immagino che qualcosa del genere potrebbe funzionare. #Non testato

public class MessageListener
{
    private readonly IObservable<IMessage> _messages;
    private readonly IScheduler _scheduler;

    public MessageListener()
    {
        _scheduler = new EventLoopScheduler();

        var messages = ListenToMessages()
                                    .SubscribeOn(_scheduler)
                                    .Publish();

        _messages = messages;
        messages.Connect();
    }

    public IObservable<IMessage> Messages
    {
        get {return _messages;}
    }

    private IObservable<IMessage> ListenToMessages()
    {
        return Observable.Create<IMessage>(o=>
        {
                return _scheduler.Schedule(recurse=>
                {
                    try
                    {           
                        var messages = GetMessages();
                        foreach (var msg in messages)
                        {
                            o.OnNext(msg);
                        }   
                        recurse();
                    }
                    catch (Exception ex)
                    {
                        o.OnError(ex);
                    }                   
                });
        });
    }

    private IEnumerable<IMessage> GetMessages()
    {
         //Do some work here that gets messages from a queue, 
         // file system, database or other system that cant push 
         // new data at us.
         // 
         //This may return an empty result when no new data is found.
    }
}

Il motivo per cui non mi piacciono davvero i soggetti è che di solito lo sviluppatore non ha un progetto chiaro sul problema. Hack in un argomento, inseriscilo qui e ovunque, e poi lascia che il povero sviluppatore del supporto indovini che WTF stava succedendo. Quando si utilizzano i metodi Crea / Genera ecc., Si localizzano gli effetti sulla sequenza. Puoi vedere tutto in un metodo e sai che nessun altro sta generando un brutto effetto collaterale. Se vedo i campi di un soggetto ora devo andare a cercare tutti i luoghi in una classe in cui viene utilizzato. Se qualche MFer ne espone uno pubblicamente, allora tutte le scommesse sono annullate, chissà come viene utilizzata questa sequenza! Async / Concurrency / Rx è difficile. Non è necessario renderlo più difficile consentendo agli effetti collaterali e alla programmazione di causalità di far girare la testa ancora di più.


10
Sto solo leggendo questa risposta ora, ma ho sentito di dover sottolineare che non avrei mai considerato di esporre l'interfaccia del Soggetto! Lo sto usando per fornire l'implementazione IObservable <> all'interno di una classe sealed (che espone IObservable <>). Posso sicuramente capire perché esporre l'interfaccia Oggetto <> sarebbe una brutta cosa ™
Anthony

hey, mi dispiace essere ottuso, ma non capisco veramente il tuo codice. cosa stanno facendo e cosa restituiscono ListenToMessages () e GetMessages ()?
user10479

1
Per il tuo progetto personale @jeromerg, questo potrebbe andare bene. Tuttavia, nella mia esperienza, gli sviluppatori lottano con WPF, MVVM, la progettazione della GUI di unit test e quindi il lancio di Rx può rendere le cose più complicate. Ho provato il modello BehaviourSubject-as-a-property. Tuttavia, ho scoperto che era molto più adottabile per gli altri se usavamo proprietà INPC standard e quindi utilizzavamo un semplice metodo di estensione per convertirlo in IObservable. Inoltre, avrai bisogno di associazioni WPF personalizzate per lavorare con i tuoi soggetti comportamentali. Ora il tuo povero team deve imparare WPF, MVVM, Rx e anche il tuo nuovo framework.
Lee Campbell

2
@LeeCampbell, per dirla in termini di esempio di codice, il modo normale sarebbe che MessageListener sia costruito dal sistema (probabilmente registri il nome della classe in qualche modo) e ti viene detto che il sistema chiamerà OnCreate () e OnGoodbye (), e chiamerà message1 (), message2 () e message3 () quando i messaggi vengono generati. Sembra che messageX [123] chiamerebbe OnNext su un argomento, ma esiste un modo migliore?
James Moore

1
@JamesMoore poiché queste cose sono molto più facili da spiegare con esempi concreti. Se conosci un'app Android Open Source che utilizza Rx e Soggetti, forse posso trovare il tempo per vedere se riesco a fornire un modo migliore. Capisco che non è molto utile stare su un piedistallo e dire che i soggetti sono cattivi. Ma penso che cose come IntroToRx, RxCookbook e ReactiveTrader diano tutti vari livelli di esempio su come usare Rx.
Lee Campbell

38

In generale dovresti evitare di usarli Subject, tuttavia per quello che stai facendo qui penso che funzionino abbastanza bene. Ho posto una domanda simile quando mi sono imbattuto nel messaggio "evitare soggetti" nei tutorial Rx.

Per citare Dave Sexton (di Rxx)

"I soggetti sono i componenti stateful di Rx. Sono utili quando è necessario creare un evento osservabile come un campo o una variabile locale."

Tendo a usarli come punto di ingresso in Rx. Quindi, se ho un codice che deve dire "è successo qualcosa" (come hai fatto tu), userei un Subjectand call OnNext. Quindi esponilo come un IObservableabbonamento a cui gli altri possono iscriversi (puoi usarlo AsObservable()sull'argomento per assicurarti che nessuno possa trasmettere a un Soggetto e rovinare le cose).

Potresti anche ottenere questo risultato con un evento .NET e usarlo FromEventPattern, ma se intendo solo trasformare l'evento in un IObservablecomunque, non vedo il vantaggio di avere un evento invece di un Subject(il che potrebbe significare che mi manca qualcosa qui)

Tuttavia, ciò che dovresti evitare abbastanza fortemente è iscriverti a un IObservablecon a Subject, cioè non passare a Subjectnel IObservable.Subscribemetodo.


Perché hai bisogno dello stato? Come mostrato dalla mia risposta, se scomponi il problema in parti separate, non devi davvero gestire lo stato. I soggetti non dovrebbero essere usati in questo caso.
casper Il

8
@casperOne Non hai bisogno di uno stato al di fuori del Subject <T> o dell'evento (che hanno entrambi raccolte di cose da chiamare, osservatori o gestori di eventi). Preferisco semplicemente usare un oggetto se l'unico motivo per aggiungere un evento è racchiuderlo in FromEventPattern. A parte un cambiamento negli schemi delle eccezioni, che potrebbe essere importante per te, non vedo un vantaggio nell'evitare il Soggetto in questo modo. Di nuovo, potrei perdere qualcos'altro qui che l'evento è preferibile al Soggetto. La menzione dello stato era solo una parte della citazione, e sembrava meglio lasciarla lì. Forse è più chiara senza quella parte?
Wilka

@casperOne - ma non dovresti nemmeno creare un evento solo per racchiuderlo in FromEventPattern. Ovviamente è un'idea terribile.
James Moore

3
Ho spiegato la mia citazione in modo più approfondito in questo post del blog .
Dave Sexton,

Tendo a usarli come punto di ingresso in Rx. Questo ha colpito nel segno per me. Ho una situazione in cui è presente un'API che, se invocata, genera eventi che vorrei far passare attraverso una pipeline di elaborazione reattiva. Il Soggetto era la risposta per me, poiché il FromEventPattern non sembra esistere in RxJava AFAICT.
scorpiodawg

31

Spesso quando gestisci un Soggetto, in realtà stai solo reimplementando funzionalità già in Rx, e probabilmente in un modo non altrettanto robusto, semplice ed estensibile.

Quando stai cercando di adattare un flusso di dati asincrono in Rx (o creare un flusso di dati asincrono da uno che non è attualmente asincrono), i casi più comuni sono di solito:

  • La fonte dei dati è un evento : come dice Lee, questo è il caso più semplice: usa FromEvent e vai al pub.

  • La fonte dei dati proviene da un'operazione sincrona e desideri aggiornamenti con polling , (ad esempio un servizio web o una chiamata al database): in questo caso potresti usare l'approccio suggerito da Lee, o per casi semplici, potresti usare qualcosa di simile Observable.Interval.Select(_ => <db fetch>). Potresti voler usare DistinctUntilChanged () per impedire la pubblicazione di aggiornamenti quando non è cambiato nulla nei dati di origine.

  • La fonte dei dati è una sorta di API asincrona che chiama il tuo callback : in questo caso, usa Observable.Create per collegare il tuo callback per chiamare OnNext / OnError / OnComplete sull'osservatore.

  • La sorgente dei dati è una chiamata che si blocca fino a quando non sono disponibili nuovi dati (ad esempio alcune operazioni di lettura sincrone del socket): in questo caso, è possibile utilizzare Observable.Create per racchiudere il codice imperativo che legge dal socket e pubblica su Observer. quando i dati vengono letti. Potrebbe essere simile a quello che stai facendo con il Soggetto.

L'uso di Observable.Create rispetto alla creazione di una classe che gestisce un Subject è abbastanza equivalente all'uso della parola chiave yield rispetto alla creazione di un'intera classe che implementa IEnumerator. Certo, puoi scrivere un IEnumerator per essere pulito e bravo come cittadino come il codice di rendimento, ma quale è meglio incapsulato e ha un design più ordinato? Lo stesso vale per Observable.Create e per i soggetti di gestione.

Observable.Create ti offre uno schema pulito per una configurazione lenta e uno smontaggio pulito. Come si ottiene questo risultato con una classe che avvolge un soggetto? Hai bisogno di un qualche tipo di metodo Start ... come fai a sapere quando chiamarlo? O lo inizi sempre, anche quando nessuno sta ascoltando? E quando hai finito, come fai a smettere di leggere dal socket / interrogare il database, ecc.? Devi avere una sorta di metodo Stop e devi comunque avere accesso non solo all'IObservable a cui sei iscritto, ma alla classe che ha creato l'oggetto in primo luogo.

Con Observable.Create, è tutto racchiuso in un unico posto. Il corpo di Observable.Create non viene eseguito finché qualcuno non si iscrive, quindi se nessuno si iscrive, non usi mai la tua risorsa. E Observable.Create restituisce un Disposable che può chiudere in modo pulito la tua risorsa / callback, ecc. - Questo viene chiamato quando Observer annulla l'iscrizione. La durata delle risorse che stai utilizzando per generare l'Osservabile è strettamente legata alla durata dell'Osservabile stesso.


1
Spiegazione molto chiara di Observable.Create. Grazie!
Evan Moran

1
Ho ancora casi in cui utilizzo un soggetto, in cui un oggetto broker espone l'osservabile (diciamo che è solo una proprietà modificabile). Diversi componenti chiameranno il broker indicando quando quella proprietà cambia (con una chiamata al metodo) e quel metodo esegue un OnNext. I consumatori si iscrivono. Penso che in questo caso utilizzerei un BehaviorSubject, è appropriato?
Frank Schwieterman,

1
Dipende dalla situazione. Un buon design Rx tende a trasformare il sistema verso un'architettura asincrona / reattiva. Può essere difficile integrare in modo pulito piccoli componenti di codice reattivo con un sistema di progettazione imperativa. La soluzione del cerotto consiste nell'usare i Soggetti per trasformare azioni imperative (chiamate di funzioni, insiemi di proprietà) in eventi osservabili. Quindi ti ritroverai con piccole sacche di codice reattivo e nessun vero "aha!" momento. Cambiare il design per modellare il flusso di dati e reagire ad esso di solito offre un design migliore, ma è un cambiamento pervasivo e richiede un cambiamento di mentalità e il consenso del team.
Niall Connaughton,

1
Vorrei affermare qui (come Rx inesperto) che: Usando i soggetti puoi entrare nel mondo della Rx all'interno di un'applicazione imperativa cresciuta e trasformarla lentamente. Anche per fare le prime esperienze .... e sicuramente in seguito cambiare il codice come avrebbe dovuto essere dall'inizio (lol). Ma per cominciare penso che potrebbe valere la pena usare i soggetti.
Robetto

9

Il blocco di testo citato spiega praticamente perché non dovresti usare Subject<T>, ma per semplificare, stai combinando le funzioni di osservatore e osservabile, iniettando una sorta di stato nel mezzo (sia che tu stia incapsulando o estendendo).

È qui che ti imbatti nei guai; queste responsabilità dovrebbero essere separate e distinte l'una dall'altra.

Detto questo, nel tuo caso specifico , ti consiglio di suddividere le tue preoccupazioni in parti più piccole.

Innanzitutto, hai il tuo thread che è caldo e monitora sempre l'hardware per i segnali per i quali generare notifiche. Come faresti normalmente? Eventi . Quindi iniziamo con quello.

Definiamo l'attivazione del EventArgstuo evento.

// The event args that has the information.
public class BaseFrameEventArgs : EventArgs
{
    public BaseFrameEventArgs(IBaseFrame baseFrame)
    {
        // Validate parameters.
        if (baseFrame == null) throw new ArgumentNullException("IBaseFrame");

        // Set values.
        BaseFrame = baseFrame;
    }

    // Poor man's immutability.
    public IBaseFrame BaseFrame { get; private set; }
}

Ora, la classe che attiverà l'evento. Nota, questa potrebbe essere una classe statica (poiché hai sempre un thread in esecuzione che monitora il buffer hardware), o qualcosa che chiami su richiesta che si iscrive a quella . Dovrai modificarlo come appropriato.

public class BaseFrameMonitor
{
    // You want to make this access thread safe
    public event EventHandler<BaseFrameEventArgs> HardwareEvent;

    public BaseFrameMonitor()
    {
        // Create/subscribe to your thread that
        // drains hardware signals.
    }
}

Quindi ora hai una classe che espone un evento. Gli osservabili funzionano bene con gli eventi. Tanto che c'è un supporto di prima classe per la conversione di flussi di eventi (si pensi a un flusso di eventi come più attivazioni di un evento) in IObservable<T>implementazioni se si segue il modello di eventi standard, attraverso il metodo staticoFromEventPattern sulla Observableclasse .

Con l'origine dei tuoi eventi e il FromEventPatternmetodo, possiamo creare IObservable<EventPattern<BaseFrameEventArgs>>facilmente un (la EventPattern<TEventArgs>classe incarna ciò che vedresti in un evento .NET, in particolare, un'istanza derivata da EventArgse un oggetto che rappresenta il mittente), in questo modo:

// The event source.
// Or you might not need this if your class is static and exposes
// the event as a static event.
var source = new BaseFrameMonitor();

// Create the observable.  It's going to be hot
// as the events are hot.
IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
    FromEventPattern<BaseFrameEventArgs>(
        h => source.HardwareEvent += h,
        h => source.HardwareEvent -= h);

Certo, vuoi un IObservable<IBaseFrame>, ma è facile, usando il Selectmetodo di estensione sulla Observableclasse per creare una proiezione (proprio come faresti in LINQ, e possiamo racchiudere tutto questo in un metodo facile da usare):

public IObservable<IBaseFrame> CreateHardwareObservable()
{
    // The event source.
    // Or you might not need this if your class is static and exposes
    // the event as a static event.
    var source = new BaseFrameMonitor();

    // Create the observable.  It's going to be hot
    // as the events are hot.
    IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
        FromEventPattern<BaseFrameEventArgs>(
            h => source.HardwareEvent += h,
            h => source.HardwareEvent -= h);

    // Return the observable, but projected.
    return observable.Select(i => i.EventArgs.BaseFrame);
}

7
Grazie per la tua risposta @casperOne, questo era il mio approccio iniziale ma mi è sembrato "sbagliato" aggiungere un evento solo per poterlo concludere con Rx. Attualmente utilizzo i delegati (e sì, so che è esattamente quello che è un evento!) Per adattarsi al codice utilizzato per il caricamento e il salvataggio della configurazione, questo deve essere in grado di ricostruire le pipeline dei componenti e il sistema dei delegati mi ha dato di più flessibilità. Rx mi sta dando un mal di testa in quest'area ora, ma il potere di tutto il resto nel framework rende molto utile risolvere il problema di configurazione.
Anthony

@Anthony Se riesci a far funzionare il suo esempio di codice, fantastico, ma come ho commentato, non ha senso. Per quanto riguarda il sentirsi "sbagliato", non so perché suddividere le cose in parti logiche sembra "sbagliato", ma non hai fornito abbastanza dettagli nel tuo post originale per indicare come tradurlo al meglio in IObservable<T>quanto nessuna informazione su come tu " stai attualmente segnalando con tale informazione.
casper Il

@casperOne Secondo te, l'uso di Subject sarebbe appropriato per un Message Bus / Event Aggregator?
kitsune

1
@kitsune No, non vedo perché lo farebbero. Se stai pensando "ottimizzazione" devi chiederti se questo è il problema o meno, hai misurato che Rx sia la causa del problema?
casperOne

2
Sono d'accordo con casperOne sul fatto che dividere le preoccupazioni è una buona idea. Vorrei sottolineare che se vai con il pattern Hardware to Event to Rx, perdi la semantica dell'errore. Qualsiasi connessione o sessione persa, ecc. Non sarà esposta al consumatore. Ora il consumatore non può decidere se desidera riprovare, disconnettersi, iscriversi a un'altra sequenza o qualcos'altro.
Lee Campbell

0

È brutto generalizzare che i Soggetti non sono buoni da usare per un'interfaccia pubblica. Sebbene sia certamente vero che questo non è il modo in cui dovrebbe apparire un approccio di programmazione reattivo, è sicuramente una buona opzione di miglioramento / refactoring per il tuo codice classico.

Se si dispone di una proprietà normale con una funzione di accesso di un set pubblico e si desidera notificare le modifiche, non si parla contro la sua sostituzione con un BehaviorSubject. L'INPC o altri eventi aggiuntivi non sono così puliti e personalmente mi stanca. A questo scopo puoi e dovresti usare BehaviorSubjects come proprietà pubbliche invece delle normali proprietà e abbandonare INPC o altri eventi.

Inoltre, l'interfaccia Soggetto rende gli utenti della tua interfaccia più consapevoli della funzionalità delle tue proprietà e sono più propensi a iscriversi invece di ottenere solo il valore.

È il migliore da usare se vuoi che gli altri ascoltino / si iscrivano ai cambiamenti di una proprietà.

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.