Cuando hablamos de programación reactiva, nos estamos refiriendo a un paradigma de programación diferente al que estamos habituados la mayoría de los desarrolladores. Esto hace que cuando nos enfrentamos por primera vez a este tipo de tecnologías, pueda resultarnos difícil asimilar los conceptos. No obstante, una vez que se entiende la dinámica, resulta bastante sencillo utilizar estas herramientas y aplicarlas a nuestro trabajo.
¿Qué es la programación reactiva?
Empecemos por entender a qué nos referimos con “programación reactiva”. ¿Qué estamos diciendo cuando nos referimos a una persona como proactiva o reactiva?
Una persona proactiva es aquella que toma la iniciativa para realizar una tarea mientras que una reactiva es la que espera a tener algún tipo de estímulo para realizarla.
El estilo habitual de programación podríamos definirlo como “proactivo”, en el sentido de que una vez que entramos una rutina, las instrucciones se ejecutan una tras otra sin esperar ningún estímulo externo.
En programación reactiva lo que hacemos es definir qué instrucciones o tareas deben llevarse a cabo cuando aparece dicho estímulo o señal. En este sentido es similar a lo que hacemos cuando programamos los elementos gráficos (botones, etc.) en una aplicación de escritorio (por ejemplo, con java swing).
Por definición, la programación reactiva es asíncrona y no bloqueante. Es decir, definimos las instrucciones a ejecutar cuando aparezca la señal (que puede ser la respuesta de un servicio web, por ejemplo) y pasamos a la siguiente instrucción. En el caso de la programación tradicional, nos veríamos obligados a esperar la respuesta a dicho servicio web antes de continuar.
¿Cómo?
Para implementar este tipo de soluciones disponemos de lo que se conoce como Streams Reactivos.
Los Streams Reactivos aparecen en Java9 (realmente Java9 simplemente se limita a introducir la especificación) y siguen el modelo de los Streams tradicionales que aparecen en Java8; ambos se apoyan en las características de programación funcional (Interfaces funcionales y lambdas) que también introduce Java8.
Sin embargo, hay una diferencia relevante entre ambos:
- Los Streams tradicionales siguen un modelo “PULL”, es decir, se recorre la secuencia de valores y se aplican las funciones/ transformaciones sobre cada uno de ellos. En el fondo sigue el patrón iterador/iterable y es el desarrollador el que define en qué momento se aplican dichas funciones.
- Los Streams reactivos siguen un modelo “PUSH”, es decir, se definen funciones de callback a las que se invoca cuando aparecen nuevos elementos (que llamaremos señales). El desarrollador define dichas funciones, pero no puede determinar cuándo se invocan. Se utiliza una evolución del patrón Observador-Observable denominado Publisher-Subscriber.
En el patrón Publisher-Subscriber, el objeto Publisher actúa como Observable y permite subscribir Subscribers que actúan como observadores. Conceptualmente, la diferencia entre uno y otro es que mientras que en el patrón Observer el objeto productor (observable) comunica los cambios a su lista de observadores de manera síncrona, en el patrón Pub-Sub el productor (Publisher) emite señales que los subscriptores van consumiendo de manera asíncrona.
Librerías de alto nivel
Los Streams Reactivos ofrecen una funcionalidad de bajo nivel, adecuada para la construcción de librerías, pero insuficiente para proporcionar la riqueza semántica que se precisa habitualmente para construir aplicaciones de cierta complejidad. Para cubrir ese déficit aparecen diferentes librerías tales como:
- Project Reactor
- RxJava2
- Akaa
- Etc
Todas estas librerías, en mayor o menor medida, nos proporcionan mecanismos no solo para ejecutar código de manera reactiva/asíncrona, sino también para combinar y transformar los resultados.
Por otro lado, Webflux es un framework reactivo que forma parte del ecosistema Spring. Webflux puede trabajar con varias de las librerías antes expuestas, aunque por defecto utiliza Project Reactor.
¿Por qué usar programación reactiva / Webflux?
Los casos de uso para los que la programación reactiva está indicada son múltiples. Obviamente hay escenarios en los que no aporta ninguna ventaja relevante o, incluso, casos en los que está contraindicado.
Como sucede con todas las tecnologías, debemos evitar dejarnos seducir por la novedad e intentar aplicarla “con calzador” a situaciones para las que no está indicada y, de igual manera, también conviene reconocer los casos en que puede darnos ciertas ventajas.
Un caso de uso posible es aquel en el que un proceso requiere consumir varios microservicios o servicios REST total o parcialmente independientes entre sí para, finalmente, combinar los resultados.
En un escenario como el descrito, es posible paralelizar las consultas mediante peticiones asíncronas e ir combinando los resultados según obtenemos las respuestas.
Caso Práctico
Antes de enunciar el caso práctico, veamos los dos tipos de Publisher que define Project Reactor:
- Flux<T>: Define un flujo de datos asíncrono, que emite un numero finito o infinito de elementos tipo T
- Mono<T>: Define un flujo de datos asíncrono, que emite un solo elemento de tipo T
Somos una gestoría fiscal y realizamos borradores de declaración de hacienda para nuestros clientes. Para ellos tenemos un servicio REST que dado un DNI nos da un JSON que representa los datos personales de nuestro cliente y sendos servicios que dado el id del cliente nos devuelve los datos bancarios y de renta, también en JSON.
Vamos a generar el borrador de dos clientes:
- WebClient es un cliente web reactivo. Cuando hacemos una petición obtenemos un Mono (o un Flux, si aplica). Cabe destacar que las llamadas que vemos son no bloqueantes, es decir, los Mono que devuelven son Publishers que emitirán los datos cuando el servidor le devuelva los datos solicitados. En tecnología Front hablaríamos de una Promise.
- Obtenemos los Mono para los datos de ambos clientes (líneas 18 y 20) y combinamos ambos en el Flux fluxClientes (línea 22). Ahora tenemos un flujo asíncrono formado por dos elementos. Obsérvese que ninguna llamada es bloqueante, tarden lo que tarden las llamadas al servidor, simplemente obtenemos la expectativa del resultado.
- Cuando llegue cada una de las respuestas emitirá el Mono correspondiente y provocará la emisión del Flux. No se respeta el orden, es decir, podrían perfectamente llegar primero los datos del cliente2.
- En la línea 26 aplicamos el operador flatMap a fluxClientes. Sin entrar en detalles, generamos un nuevo Flux, en este caso de borradores. Cada vez que emita el Flux de clientes, obtendremos la renta y los datos bancarios del cliente dado (líneas 27 y 28) y generamos un Mono de borrador; el operador zip ensambla un Mono<Borrador> a partir Mono<Renta> y Mono<Banco> y emite cuando ambos hayan emitido, combinando sus valores. De nuevo todo es asíncrono, las llamadas al servidor se van ensamblando en paralelo (allá donde se pueda) y cuando todos los datos estén disponibles se combinan.
- Finalmente (línea 33), implementamos un Subscriber mediante una lambda, y cada vez que se emite un borrador llamamos a generarBorrador, método privado que imprime el borrador.