¿Cómo puedo ayudar a mi equipo a amar RxJS?

Comprender la causa y el efecto es el núcleo de la comprensión de cualquier aplicación, y la programación reactiva destruye la capacidad de verla con claridad. Pero también es realmente poderoso. Entonces, ¿cómo podemos ayudar a nuestros colegas desarrolladores a disfrutar trabajando con él?

No a todos les gusta la programación reactiva. Muchos desarrolladores prefieren la programación imperativa porque es simple y directa. Si hace clic en un botón y los datos en 3 lugares de la página deben cambiar, entonces ... ¡tomemos esos 3 lugares y cambiemos el texto!

Con cada vez más personas que usan Redux y RxJS, muchas aplicaciones son cada vez más difíciles de entender, depurar y desarrollar. Estas tecnologías ayudan a prevenir errores que surgen del estado inconsistente, pero la compensación es la gran cantidad de indirecta que introducen.

Tome este ejemplo: en una aplicación de chat, un desarrollador puede ver que cuando hace clic en el botón "Abandonar chat", el hilo de chat se elimina de la lista de hilos de chat en la vista, pero el websocket no se cierra correctamente. La arquitectura reactiva ahora obstaculiza la comprensión de por qué sucede esto. Cuando el desarrollador mira el controlador de eventos para Salir del chat, solo ve this.store.dispatch ({type: 'LEAVE_CHAT', id}). Después de rastrear el código durante un tiempo, finalmente llegan a la causa de que el elemento de chat desaparezca de la lista. Les parece que identificar el mecanismo de causa y efecto en el proceso no debería haber sido tan difícil. Esperaban algo en el controlador de eventos como this.chatService.removeChatFromList ({id}); para que también puedan agregar this.chatService.ohAndCloseThisWebSocketPlz ({id}) ;. ¡Pero no estaba allí!

¿Los partidarios de la programación reactiva son sádicos?

Tal vez. Pero si está acostumbrado a la programación reactiva, es posible que haya notado que el error en ese ejemplo no habría existido en primer lugar si el código hubiera sido realmente reactivo. La lista de chat se habría suscrito a la lista central de identificadores de chat, al igual que los observables responsables de abrir y cerrar conexiones de socket web, y ambos habrían reaccionado en el momento adecuado. El manejador de acciones no habría necesitado recordar ambos.

Hacer todo de manera imperativa significa asegurarse de haber tenido en cuenta todas las combinaciones posibles de efectos que pueden resultar cuando maneja una acción. La programación imperativa puede ser directa y fácil de seguir, pero una vez que una aplicación alcanza un cierto nivel de complejidad, existe una gran posibilidad de que los desarrolladores olviden pequeñas cosas aquí y allá, y el estado inconsistente comienza a dar cuenta de la mayoría de los errores.

El código imperativo también puede tener consecuencias inesperadas. Una característica oscura puede cambiar repentinamente el estado del que depende otra característica, o el estado puede mutar en el orden incorrecto, abriendo un portal al infierno y requiriendo que recolectemos tarjetas de varios colores para encontrar un portal de regreso.

Tarjetas de varios colores.

Pero la mayoría de los desarrolladores todavía prefieren código directo e imperativo. Entonces, ¿qué hacen cuando el mandato viene de arriba para usar esta arquitectura indirecta de Redux / RxJS?

El resultado más probable es que los desarrolladores comenzarán a ver el estado y la tienda como simplemente un paso adicional en el proceso de hacer que las cosas sucedan. Entonces comienzan a hacer árboles de estado como este:

// No hagas esto:
Estado de la interfaz: {
  showModal: boolean;
  loadItemDetails: boolean;
  showLoadingIcon: boolean;
  cancelRequest: boolean;
  hidePopup: boolean;
  navigationToPage: cadena;
  cleanRoom: boolean;
  goOutside: boolean;
  doEverything: boolean;
  participar en la filantropía: booleana;
}

El estado se convierte efectivamente en un vehículo incómodo para los comandos. Pero en realidad se supone que es una instantánea de la información a partir de la cual se puede generar una sola vista. Entonces, ¿dónde nos pone eso? Peor de lo que estábamos con la simple programación imperativa, porque ahora no solo tenemos toda esta ridícula maquinaria Redux / RxJS en todas partes, sino que aún tenemos que recordar los efectos de todos estos comandos.

(Nota: RxJS se puede usar sin Redux, por supuesto, pero sin Redux es probable que todavía tenga el problema de que los comandos se emitan como valores observables si no está codificando de forma reactiva).

Es muy difícil cambiar de un estado mental imperativo a uno reactivo, por lo que realmente debes desearlo. Muchos desarrolladores no están interesados ​​en pensar de manera reactiva, porque todavía no han experimentado mucho dolor por el estado inconsistente, o tal vez no se han dado cuenta de que el estado inconsistente es la característica común entre muchos de los errores que han encontrado.

Cuando elegir la programación reactiva

Dado que es contraproducente impulsar la programación reactiva cuando falta la motivación, la programación reactiva parece tener sentido en estas situaciones:

  1. El valor de la programación reactiva en el proyecto es lo suficientemente evidente como para que todos los miembros del equipo deseen utilizarlo, a pesar de sus inconvenientes.
  2. El proyecto es moderadamente adecuado para él, el equipo no se opone particularmente a él y tiene buenas maneras de garantizar patrones reactivos (como tener solo un estado único en la tienda, usar selectores para datos derivados y evitar el estado local)
  3. El costo de la programación reactiva se puede reducir de alguna manera para que incluso los equipos que trabajan en proyectos simples quieran usarlo

Dado que la programación reactiva puede simplificar dramáticamente las aplicaciones complejas, la tercera situación es ideal en mi opinión. Si de alguna manera pudiéramos reducir el costo de la arquitectura reactiva, los equipos podrán adoptarla mientras sus aplicaciones aún sean pequeñas, antes de que pierdan grandes cantidades de recursos persiguiendo incendios causados ​​por un estado mutable compartido.

Disminuyendo el costo reactivo

La codificación aumenta de forma reactiva la dificultad de resolver un problema la primera vez y comprender una solución existente. Afortunadamente, muchos en la comunidad están trabajando en herramientas para abordar estos desafíos. Para el resto de esta publicación, me gustaría compartir algo de lo que más me entusiasma, y ​​también algunos de mis pensamientos sobre cómo podríamos hacer que sea más fácil entender y crear código reactivo.

Comprender el código reactivo

La clave para comprender el código reactivo es recuperar la transparencia de causa y efecto en la aplicación. Aquí hay algunas herramientas que creo que hacen un gran trabajo en esto:

  • Redux-devtools. Una fuente de indirección en las aplicaciones de Redux son las acciones, que separan lo que sucedió de cómo se supone que cambia el estado (el estado "reacciona" a las acciones). Redux-devtools vuelve a conectar estos conceptos al mostrar cómo las acciones cambian de estado. Aquí hay un buen video que muestra lo que puede hacer.
  • Andre Staltz ha creado una herramienta de visualización ordenada para observables en su biblioteca JS, CycleJS. Puede ver los datos que fluyen a través de su aplicación a la velocidad que elija. Me encantaría ver algo así para las aplicaciones RxJS.
  • Inspirado por Andre Staltz y otros, este sitio web le permite visualizar cualquier cadena observable. Creo que sería increíble como una extensión VSCode.

Estas son herramientas increíbles, y todavía hay muchas oportunidades para mejorar la transparencia del código reactivo. Si tiene alguna idea, deje un comentario a continuación y tal vez usted o alguien pueda recoger una idea y ejecutarla.

Resolviendo reactivamente

La dificultad para resolver un problema de manera reactiva es que necesita tener tantas cosas en la cabeza que desborde rápidamente su capacidad y tenga que comenzar de nuevo, tal vez varias veces antes de que finalmente lo resuelva. Se siente similar a cuando alguien le pide que resuelva un problema matemático como este en su cabeza:

32058
X 17
-----

Es difícil recordar todos los números por sí solo, pero también recordar los pasos intermedios y luego calcular las interacciones entre los números es más de lo que la mayoría de las personas pueden hacer.

Por lo tanto, es bueno que hacerlo en tu cabeza no sea la única forma de resolver este problema. Hay un método más fácil que implica escribir varios pasos pequeños. Este proceso aleja el problema principal de las limitaciones de la memoria de trabajo a un proceso más confiable que se puede aprender y enseñar.

¿Existe un proceso similar para la codificación reactiva?

Tomemos un ejemplo ordinario y comparemos las soluciones imperativas y reactivas, y veamos si no podemos extraer un proceso de la solución reactiva que podamos usar para ayudarnos a resolver problemas más complejos.

Ejemplo básico: autocompletado asíncrono

La mayoría de los desarrolladores están bastante familiarizados con este ejemplo. Un usuario escribe algo de texto en una entrada, se obtiene una lista filtrada de datos que contiene la cadena que escribió y los resultados se muestran al usuario.

La solución imperativa es sencilla. Comenzamos con el controlador de eventos (cambio). Esta función tomará el término de búsqueda como entrada, lo pasará a una función que recupera datos, luego, cuando los datos regresen, los vinculará a la clase de componente para que se muestre la plantilla. Aquí hay un ejemplo de implementación de esto:

Autocompletar implementado imperativamente

Excelente. Eso no fue demasiado difícil.

La solución reactiva es más difícil de describir, pero este fue mi proceso de pensamiento: los datos deben recuperarse después de que un usuario teclee, por lo que el observable que obtiene datos está buscando la entrada del usuario. La entrada del usuario será un Asunto que tenga el método .next llamado en el controlador de eventos (cambio), por lo que parece que el método de obtención de datos tendrá que desconectar el Mapa de ese asunto. Pero esa secuencia debe asignarse como una propiedad en el componente para que esté disponible para la canalización asíncrona, de modo que ahí es donde realmente comienza nuestro código. Escribamos todo eso antes de que olvidemos algo:

Autocompletar implementado reactivamente

Eso no estuvo tan mal. Y dado que esto es reactivo, ahora podemos desactivar la detección de cambios para este componente, y no descargaremos datos para términos de búsqueda anteriores. Esto garantiza que los resultados nunca se desincronicen con el término de búsqueda. (Aquí está el proyecto StackBlitz con las 2 implementaciones).

¿Podríamos haber llegado a esta solución un poco más suave, sin tener que tener tanto en cuenta a la vez?

Cuando comparamos estas dos soluciones una al lado de la otra, podemos ver que las soluciones imperativas y reactivas asocian diferentes pasos en el proceso:

La programación imperativa asocia el evento al efecto inmediato, mientras que la programación reactiva asocia el efecto final a su fuente de datos inmediata. Ben Lesh y otros han dicho algunas veces que "pensar reactivamente" es como pensar al revés o al revés. Si observa la solución reactiva y comienza primero al final, notará que los datos públicos $ = parte podrían haberse escrito sin siquiera considerar el controlador de eventos (cambio).

La clave, entonces, es pensar primero en el consumidor final, que es la tubería asíncrona en la plantilla. Quiere los datos filtrados, por lo que comienza con los datos $ =. La fuente inmediata de los datos filtrados, searchTerm => fetchData (searchTerm) necesita un término de búsqueda. Sin preocuparnos de dónde proviene el término de búsqueda, asumiremos que hay un observable del que podemos encadenar. Así que simplemente escribiremos searchTerm $ antes de definirlo y lo encadenaremos con un switchMap (). Luego podemos averiguar dónde se van a bombear esos términos de búsqueda en ese observable configurando el controlador de eventos para (cambio). Entonces, al pensar en retrospectiva, podemos diseñar una solución con una cosa menos a la que hacer un seguimiento a la vez.

Encapsulemos este proceso en un par de pasos:

  1. Identificar al consumidor.
  2. Nombra un observable para encadenar, sin preocuparte por lo que pondrá valores en él
  3. Conecta al consumidor con este nuevo observable
  4. Defina el primer observable y de dónde obtiene sus propios valores

Si necesitamos manejar múltiples eventos asincrónicos que ocurren en serie, podemos repetir los Pasos 1–3 para cada cadena observable, hasta llegar al primer observable (Paso 4).

Ahora quiero probar este proceso en un problema mucho más complejo. Pero esto se está haciendo muy largo, así que lo dejaré para mi próxima publicación.

Conclusión

La programación reactiva puede ayudarnos a evitar muchos errores costosos, pero sentirnos cómodos es un obstáculo importante. Creo que la comunidad de desarrollo web continuará descubriendo soluciones que harán que la programación reactiva sea mucho más fácil, y esto les ahorrará a los desarrolladores mucha frustración, tiempo y recursos.

¡Gracias por leer! Si viste mi última publicación, tal vez recuerdes al final que dije que mi próxima publicación se basaría en ella. Estaba trabajando en esa publicación cuando me desvié de esta publicación. Entonces esa otra publicación aún está por llegar.

De todos modos, ¡comparte tus pensamientos en los comentarios! ¿Qué otras herramientas conoces? ¿Qué le gustaría ver? ¿Has utilizado un proceso como el que describí aquí? ¿Qué otros consejos tienes?