Come posso aiutare il mio team ad amare RxJS?

Comprendere la causa e l'effetto è al centro della comprensione di qualsiasi applicazione e la programmazione reattiva distrugge la capacità di vederla chiaramente. Ma è anche molto potente. Quindi, come possiamo aiutare i nostri colleghi sviluppatori a divertirsi lavorando con esso?

Non a tutti piace la programmazione reattiva. Molti sviluppatori preferiscono la programmazione imperativa perché è semplice e diretta. Se fai clic su un pulsante e i dati in 3 punti della pagina devono cambiare, allora ... prendiamo quei 3 posti e cambiamo il testo!

Con sempre più persone che utilizzano Redux e RxJS, molte applicazioni stanno diventando sempre più difficili da comprendere, eseguire il debug e sviluppare. Queste tecnologie aiutano a prevenire i bug derivanti da uno stato incoerente, ma il compromesso è l'enorme quantità di indirettezza che introducono.

Prendi questo esempio: in un'applicazione di chat, uno sviluppatore potrebbe vedere che quando fa clic su un pulsante "Lascia chat", il thread della chat viene rimosso dall'elenco dei thread della chat nella vista, ma il websocket non viene chiuso correttamente. L'architettura reattiva ora ostacola la comprensione del perché ciò sta accadendo. Quando lo sviluppatore esamina il gestore eventi per Leave Chat, vede solo this.store.dispatch ({type: 'LEAVE_CHAT', id}). Dopo aver rintracciato il codice per un po ', finalmente arrivano alla causa dell'elemento della chat che scompare dall'elenco. A loro sembra che identificare il meccanismo di causa-effetto nel processo non avrebbe dovuto essere così difficile. Si aspettavano qualcosa nel gestore dell'evento come this.chatService.removeChatFromList ({id}); in modo che possano anche aggiungere this.chatService.ohAndCloseThisWebSocketPlz ({id}) ;. Ma non c'era!

I sostenitori della programmazione reattiva sono sadici?

Può essere. Ma se sei abituato alla programmazione reattiva, potresti aver notato che il bug in quell'esempio non sarebbe esistito in primo luogo se il codice fosse stato davvero reattivo. L'elenco delle chat si sarebbe iscritto all'elenco centrale degli ID chat, così come gli osservabili responsabili dell'apertura e della chiusura delle connessioni ai socket web, ed entrambi avrebbero reagito al momento giusto. Il gestore dell'azione non avrebbe dovuto ricordare entrambi.

Fare tutto in modo imperativo significa assicurarsi di aver tenuto conto di ogni possibile combinazione di effetti che possono derivare quando si maneggia un'azione. La programmazione imperativa può essere diretta e facile da seguire, ma una volta che un'app raggiunge un certo livello di complessità, è molto alta la possibilità che gli sviluppatori dimentichino piccole cose qua e là e lo stato incoerente inizi a tenere conto della maggior parte dei bug.

Il codice imperativo può comportare anche conseguenze impreviste. Una caratteristica oscura potrebbe cambiare improvvisamente lo stato da cui dipende un'altra funzione, oppure lo stato potrebbe essere mutato nell'ordine sbagliato, strappando un portale all'inferno e richiedendoci di raccogliere chiavi magnetiche di vari colori per ritrovare un portale.

Keycard di vari colori

Ma la maggior parte degli sviluppatori preferisce ancora fortemente il codice diretto e imperativo. Quindi cosa fanno quando il mandato viene dall'alto per utilizzare questa architettura indiretta Redux / RxJS?

Il risultato più probabile è che gli sviluppatori inizieranno a vedere lo stato e il negozio come semplicemente un ulteriore passo nel processo di far accadere le cose. Quindi iniziano a creare alberi di stato come questo:

// Non farlo:
stato dell'interfaccia: {
  showModal: boolean;
  loadItemDetails: booleano;
  showLoadingIcon: boolean;
  cancelRequest: booleano;
  hidePopup: booleano;
  navigateToPage: string;
  cleanRoom: booleano;
  goOutside: booleano;
  doEverything: booleano;
  engagementInPhilanthropy: booleano;
}

Lo stato diventa effettivamente un veicolo scomodo per i comandi. Ma in realtà si suppone che sia un'istantanea delle informazioni da cui è possibile generare una sola vista. Quindi dove ci mette questo? Peggio di quanto non fossimo con la semplice programmazione imperativa, perché non solo ora abbiamo tutti questi ridicoli macchinari Redux / RxJS ovunque, ma dobbiamo ancora ricordare gli effetti di tutti questi comandi.

(Nota: RxJS può essere usato senza Redux ovviamente, ma senza Redux probabilmente avrai ancora il problema che i comandi vengono emessi come valori osservabili se non stai codificando in modo reattivo.)

È molto difficile passare da un imperativo a uno stato d'animo reattivo, e quindi devi davvero volerlo. Molti sviluppatori non sono interessati a pensare in modo reattivo, perché non hanno ancora provato molto dolore a causa di uno stato incoerente o forse non hanno notato che lo stato incoerente è la comunanza tra molti dei bug che hanno riscontrato.

Quando scegliere la programmazione reattiva

Dal momento che è controproducente spingere per la programmazione reattiva quando manca la motivazione, la programmazione reattiva sembra avere senso in queste situazioni:

  1. Il valore della programmazione reattiva nel progetto è abbastanza evidente che tutti i membri del team desiderano usarlo, nonostante i suoi svantaggi
  2. Il progetto è moderatamente adatto a questo, il team non è particolarmente contrario ad esso e hai buoni modi per garantire modelli reattivi (come avere solo uno stato unico nel negozio, usare selettori per i dati derivati ​​ed evitare lo stato locale)
  3. Il costo della programmazione reattiva può in qualche modo essere ridotto in modo che anche i team che lavorano su progetti semplici vorranno utilizzarlo

Poiché la programmazione reattiva può semplificare notevolmente le applicazioni complesse, la terza situazione è ideale secondo me. Se potessimo in qualche modo ridurre il costo dell'architettura reattiva, i team saranno in grado di adottarla mentre le loro app sono ancora piccole, prima che perdano enormi quantità di risorse che inseguono gli incendi causati da uno stato condiviso e mutevole.

Diminuendo il costo reattivo

La codifica aumenta in modo reattivo la difficoltà di risolvere il problema la prima volta e di comprendere una soluzione esistente. Fortunatamente, molti nella comunità stanno lavorando su strumenti per affrontare queste sfide. Per il resto di questo post vorrei condividere alcune delle cose di cui sono più entusiasta, e anche alcuni dei miei pensieri su come potremmo facilitare la comprensione e la creazione di codice reattivo.

Comprensione del codice reattivo

La chiave per comprendere il codice reattivo è ripristinare la trasparenza di causa ed effetto nell'applicazione. Ecco alcuni strumenti che credo facciano un ottimo lavoro in questo:

  • Redux-DevTools. Una fonte di riferimento indiretto nelle app Redux sono le azioni, che separano ciò che è accaduto dal modo in cui lo stato dovrebbe cambiare (lo stato "reagisce" alle azioni). Redux-devtools ricollega questi concetti mostrando come le azioni cambiano stato. Ecco un bel video che mostra cosa può fare.
  • Andre Staltz ha creato uno strumento di visualizzazione pulito per gli osservabili nella sua libreria JS, CycleJS. Puoi guardare i dati che fluiscono attraverso la tua app alla velocità che preferisci. Mi piacerebbe vedere qualcosa di simile per le app RxJS.
  • Ispirato da Andre Staltz e altri, questo sito Web consente di visualizzare qualsiasi catena osservabile. Penso che sarebbe sorprendente come estensione VSCode.

Questi sono strumenti fantastici e ci sono ancora molte opportunità per migliorare la trasparenza del codice reattivo. Se hai qualche idea, ti preghiamo di lasciare un commento qui sotto e forse tu o qualcuno potete prendere un'idea e correre con essa.

Risolvendo reattivamente

La difficoltà a risolvere un problema in modo reattivo è che devi tenere così tante cose nella tua testa che traboccare rapidamente la tua capacità e ricominciare da capo, forse più volte prima di capirlo. È simile a quando qualcuno ti chiede di risolvere un problema di matematica come questo nella tua testa:

32058
X 17
-----

Ricordare tutti i numeri da soli è difficile, ma poi anche ricordare i passaggi intermedi e quindi calcolare le interazioni tra i numeri è solo più di quanto la maggior parte delle persone sia in grado di fare.

Quindi è una buona cosa che farlo nella tua testa non sia l'unico modo per risolvere questo problema. Esiste un metodo più semplice che prevede la scrittura di diversi piccoli passaggi. Questo processo sposta il problema principale dai limiti della memoria di lavoro a un processo più affidabile che può essere appreso e insegnato.

Esiste un processo simile per la codifica reattiva?

Facciamo un esempio ordinario e confrontiamo le soluzioni imperative e reattive e vediamo se non siamo in grado di estrarre un processo dalla soluzione reattiva che potremmo utilizzare per aiutarci a risolvere problemi più complessi.

Esempio di base: Completamento automatico asincrono

La maggior parte degli sviluppatori conosce abbastanza bene questo esempio. Un utente digita del testo in un input, viene recuperato un elenco filtrato di dati contenente la stringa digitata e i risultati vengono mostrati all'utente.

La soluzione imperativa è semplice. Iniziamo con il gestore (modifica) dell'evento. Questa funzione prenderà il termine di ricerca come input, lo passerà a una funzione che recupera i dati, quindi quando i dati ritornano li legherà alla classe del componente per la visualizzazione del modello. Ecco un esempio di implementazione di questo:

Completamento automatico implementato imperativamente

Grande. Non è stato troppo difficile.

La soluzione reattiva è più difficile da descrivere, ma questo è stato il mio processo di pensiero: i dati devono essere recuperati dopo che un utente digita, quindi l'osservabile che recupera i dati sta monitorando l'input dell'utente. L'input dell'utente sarà un soggetto che ha il metodo .next chiamato nel gestore di eventi (modifica), quindi sembra che il metodo di recupero dei dati debba disattivare la mappa di quell'oggetto. Ma quel flusso dovrebbe essere assegnato come proprietà sul componente in modo che sia disponibile per la pipe asincrona, quindi è lì che inizia davvero il nostro codice. Scriviamo tutto questo prima di dimenticare qualcosa:

Completamento automatico implementato in modo reattivo

Non è stato poi così male. E poiché questo è reattivo, ora possiamo disattivare il rilevamento delle modifiche per questo componente e non scaricare i dati per i termini di ricerca precedenti. Ciò garantisce che i risultati non verranno mai sincronizzati con il termine di ricerca. (Ecco il progetto StackBlitz con le 2 implementazioni.)

Potremmo essere arrivati ​​a questa soluzione un po 'più agevolmente, senza dover tenere così tanto in mente contemporaneamente?

Quando confrontiamo queste due soluzioni fianco a fianco, possiamo vedere che le soluzioni imperative e reattive associano insieme diversi passaggi nel processo:

La programmazione imperativa associa l'evento all'effetto immediato, mentre la programmazione reattiva associa l'effetto finale alla sua fonte di dati immediata. Ben Lesh e altri hanno affermato alcune volte che "pensare in modo reattivo" è come pensare al contrario o al contrario. Se osservi la soluzione reattiva e inizi prima alla fine, noterai che i dati pubblici $ = part avrebbero potuto essere scritti senza nemmeno considerare il gestore (modifica) dell'evento.

La chiave, quindi, è pensare prima al consumatore finale, che è la pipa asincrona nel modello. Vuole i dati filtrati, quindi inizi con i dati $ =. La fonte immediata dei dati filtrati, searchTerm => fetchData (searchTerm) necessita di un termine di ricerca. Senza preoccuparci della provenienza del termine di ricerca, supponiamo che ce ne sia un osservabile che possiamo mettere in catene. Quindi scriveremo searchTerm $ prima di averlo definito e interromperlo con un switchMap (). Quindi possiamo capire dove quei termini di ricerca verranno inseriti in quello osservabile impostando il gestore eventi per (modifica). Quindi, pensando al contrario, siamo in grado di progettare una soluzione con una cosa in meno di cui tenere traccia alla volta.

Incapsuliamo questo processo in un paio di passaggi:

  1. Identificare il consumatore
  2. Dai un nome a un osservabile di cui non devi preoccuparti, senza preoccuparti di cosa metterà dei valori al suo interno
  3. Collega il consumatore con questo nuovo osservabile
  4. Definire il primo osservabile e da dove ottiene i propri valori

Se dobbiamo gestire più eventi asincroni che si verificano in serie, possiamo ripetere i passaggi 1-3 per ciascuna catena osservabile, fino a raggiungere il primo osservabile (passaggio 4).

Ora voglio provare questo processo su un problema molto più complesso. Ma questo sta diventando molto lungo, quindi lo lascerò per il mio prossimo post.

Conclusione

La programmazione reattiva può aiutarci a evitare molti bug costosi, ma sentirsi a proprio agio con esso è un grosso ostacolo. Credo che la comunità di sviluppo web continuerà a scoprire soluzioni che renderanno molto più semplice la programmazione reattiva e questo farà risparmiare agli sviluppatori molta frustrazione, tempo e risorse.

Grazie per aver letto! Se hai visto il mio ultimo post, alla fine potresti ricordare che ho detto che il mio prossimo post si sarebbe sviluppato. Stavo lavorando su quel post quando sono stato messo da parte con questo post. In modo che l'altro post stia ancora arrivando.

Ad ogni modo, ti preghiamo di condividere i tuoi pensieri nei commenti! Quali altri strumenti conosci? Cosa ti piacerebbe vedere? Hai usato un processo come quello che ho descritto qui? Quali altri suggerimenti hai?