Programación reactiva, un primer enfoque

Programación funcional y reactiva - Computación

Profesor: Ing. Santiago Quiñones

Docente Investigador

Departamento de Ingeniería Civil

Contenidos

Introducción a la programación reactiva

Programación reactiva

Reactiva: que produce una reacción

Acciones sobre elementos de la GUI

Aproximaciones

Programación reactiva

Manejo de datos

  • Flujo infinitos de datos
  • Se reacciona cuando llega un grupo de datos
  • Reacciones de manera asíncrona
  • Patrón observer

 

Enfoques

Programación reactiva

Functional reactive programming

  • Comportamiento o señales
  • Comportamientos son valores continuos que siempre tienen un valor actual. Ej. la posición del mouse

 

Enfoques

Patrón Observer

Patrón Observer

Patrón Observer

😔Problema

Cliente

Tienda

Patrón Observer

😊Solución

Patrón Observer

🏢Estructura

Patrón Observer

🏢Estructura

Patrón Observer

🏢Estructura

Patrón Observer en Java

Observable

Lenguajes de programación

Observer

Interfaz

public interface Observer {
    void update();
}

Observable

Interfaz

public interface Observable {
    void addObserver(Observer o);

    void deleteObserver(Observer o);

    void notifyObservers();
}

EjemploObservable

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class EjemploObservable implements Observable{
    Set<Observer> observersSet = new HashSet<>();


    @Override
    public void addObserver(Observer o) {
        observersSet.add(o);
    }

    @Override
    public void deleteObserver(Observer o) {
        observersSet.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observersSet) {
            observer.update();
        }
    }
}

Ejemplo1Observer


public class Ejemplo1Observer implements Observer {
    @Override
    public void update() {
        System.out.println("Se ha llamada al ejemplo 1");
    }
}

Main

public class Main {
    public static void main(String[] args) {

        EjemploObservable observable = new EjemploObservable();

        observable.addObserver(new Ejemplo1Observer());
        
        observable.notifyObservers();

        
    }
}

Reactive Streams

Reactive Streams

Flujo de datos

Reactive Streams

Flujo de datos

Reactive Streams

Lenguaje de programación

https://reactivex.io

ReactiveX

An API for asynchronous programming with observables streams

Reactive Streams

Lenguaje de programación

Algunos programas tienen sus propias implementaciones para manejar reactive streams. 

Por ejemplo: Java

  • Desde Java 9 existe un soporte nativo para reactive streams.
  • java.util.current.Flow
    • Publisher - suscribe(), submit(), close()
    • Subscriber - onSuscribe(), onNext(), onError(), y onComplete()

Reactive Streams

Lenguaje de programación

En Scala

  • En Scala se puede trabajar con RxScala, Akka Streams, Monix, y ZIO
  • Para trabajar con RxScala en IntellyJ es necesario agregar la siguiente dependencia en build.sbt

 

libraryDependencies ++= Seq( "io.reactivex" % "rxscala_2.13" % "0.27.0" )

ReactiveX en Scala

RxScala

Trabajar con RxScala

Instalar la dependencia de RxScala: RxScala está basado en RxJava, por lo que necesitas agregar la dependencia a tu archivo build.sbt:

libraryDependencies ++= Seq( "io.reactivex" % "rxscala_2.13" % "0.27.0" )

Características principales

  • Observables: Emiten datos de forma sincrona o asincrona
  • Operadores: Permiten transformar, filtrar y combinar flujos de datos. 
  • Organizadores(Shedulers): Ayudan a manejar hilos de ejecución y concurrencia.

Reactive Streams

Flujo de datos

Ejemplo Básico: Crear un observable

Observable que emite una secuencia de valores

import rx.lang.scala.Observable

object BasicExample extends App {
  // Crear un Observable con una lista de valores
  val observable = Observable.just(1, 2, 3, 4, 5)

  // Suscribirse al Observable
  observable.subscribe(
    onNext = x => println(s"Received: $x"),        // Procesar cada valor emitido
    onError = e => println(s"Error: ${e.getMessage}"), // Manejar errores
    onCompleted = () => println("Completed!")        // Notificar cuando el flujo termine
  )
}



Clase principal para manejar flujos de datos reactivos

Crea un Observable que emite una lista de valores

Activa el Observable y le indica qué hacer con los datos que emite

Transformar datos con map

Multiplicar cada valor emitido por 2

import rx.lang.scala.Observable

object MapExample extends App {
  // Crear un Observable con valores
  val observable = Observable.just(1, 2, 3, 4, 5)

  // Aplicar transformación a cada valor
  observable
    .map(_ * 2) // Multiplica cada valor por 2
    .subscribe(x => println(s"Transformed: $x"))
}




Filtrar valores con filter

Mostrar sólo los números mayores a 3.

import rx.lang.scala.Observable

object FilterExample extends App {
  // Crear un Observable con valores
  val observable = Observable.just(1, 2, 3, 4, 5)

  // Filtrar valores mayores a 3
  observable
    .filter(_ > 3) // Filtrar números mayores a 3
    .subscribe(x => println(s"Filtered: $x"))
}




Flujos Asíncronos con temporizadores

Emitir un valor cada segundo

import rx.lang.scala.Observable

// Proporciona utilidades para trabajar con intervalos de tiempo
import scala.concurrent.duration._

object IntervalExample extends App {
  // Crear un Observable que emite valores cada segundo
  val observable = Observable.interval(1.second).take(5) // Tomar 5 valores

  // Suscribirse al flujo
  observable.subscribe(
    x => println(s"Tick: $x"), // Procesar cada emisión
  )

  // Mantener la aplicación en ejecución para observar los valores
  Thread.sleep(6000)
}





Crea un Observable que emite valores (0, 1, 2, ...) con un intervalo de 1 segundo entre cada emisión

Combinar flujos con merge

Combinar dos flujos diferentes

import rx.lang.scala.Observable

object MergeExample extends App {
  // Crear dos Observables
  val observable1 = Observable.just("A", "B", "C")
  val observable2 = Observable.just("1", "2", "3")

  
  observable1.merge(observable2)
    .subscribe(x => println(s"Received: $x"))
}




Manejo de errores

Continuar el flujo incluso si ocurre un error.

import rx.lang.scala.Observable

object ErrorHandlingExample extends App {
  // Crear un Observable que puede generar un error
  val observable = Observable.just(10, 5, 0, 4)

  // Manejar errores en el flujo
  observable
    .map(x => 10 / x) // Esto generará un error al dividir por 0
    .onErrorResumeNext(_ => Observable.just(-1)) // Continuar con un valor predeterminado
    .subscribe(
      x => println(s"Received: $x"),
      e => println(s"Error: ${e.getMessage}"),
      () => println("Stream completed")
    )
}






Simulación de un sensor

Emitir valores aleatorios como si fueran datos de un sensor.

import rx.lang.scala.Observable
import scala.util.Random
import scala.concurrent.duration._

object SensorExample extends App {
  // Crear un flujo de datos del sensor
  val sensorStream = Observable.interval(1.second).map(_ => Random.between(20.0, 30.0))

  // Procesar el flujo del sensor
  sensorStream
    .take(5) // Emitir solo 5 valores
    .subscribe(
      x => println(f"Sensor reading: $x%.2f°C"),
      e => println(s"Error: ${e.getMessage}"),
      () => println("Sensor stopped")
    )

  // Mantener la aplicación corriendo
  Thread.sleep(6000)
}

Combinar datos de sensores con zip

Combinar lecturas de dos sensores en un flujo único



import rx.lang.scala.Observable
import scala.util.Random
import scala.concurrent.duration._

object ZipExample extends App {
  // Crear flujos de dos sensores
  val sensor1 = Observable.interval(1.second).map(_ => Random.between(20, 30))
  val sensor2 = Observable.interval(1.second).map(_ => Random.between(30, 40))

  // Combinar los flujos con zip
  sensor1.zip(sensor2)
    .take(5)
    .subscribe(
      onNext = { case (temp1, temp2) => println(s"Sensor1: $temp1°C, Sensor2: $temp2°C") },
      onError = e => println(s"Error: ${e.getMessage}"),
      onCompleted = () => println("Stream completed!")
    )

  // Mantener la aplicación corriendo
  Thread.sleep(6000)
}

Combinar datos de sensores con zip

Combinar lecturas de dos sensores en un flujo único



import rx.lang.scala.Observable
import scala.util.Random
import scala.concurrent.duration._

object ZipExample extends App{
  // Crear flujos de dos sensores
  val sensor1 = Observable.interval(1.second).map(_ => Random.between(20, 30))
  val sensor2 = Observable.interval(1.second).map(_ => Random.between(30, 40))

  // Forma más "pura" de usar zipWith
  val flujoCombinado = sensor1.zipWith(sensor2) { (t1, t2) =>
      // Retornamos un String formateado
      s"Lectura combinada: Sensor1=$t1 - Sensor2=$t2" 
  }

  flujoCombinado.take(5).subscribe(mensaje => println(mensaje))

  // Mantener la aplicación corriendo
  Thread.sleep(6000)
}

Sistema de alertas

Generar una alerta si un valor supera un umbral.

import rx.lang.scala.Observable
import scala.util.Random
import scala.concurrent.duration._

object AlertExample extends App{
  val observable = Observable.interval(1.second).map(_ => Random.between(20.0, 40.0))

  observable
    .take(10)
    // 1. doOnNext: Efecto secundario. Imprime TODO lo que pasa sin modificar el flujo.
    .doOnNext(temp => println(f"Temperature: $temp%.2f°C")) 
    
    // 2. filter: Aquí aplicamos la lógica de negocio. Solo pasan los mayores a 35.
    .filter(temp => temp > 35) 
    
    // 3. map: Preparamos el mensaje de alerta (transformación).
    .map(temp => f"🚨 ALERT! High temperature detected: $temp%.2f°C")
    
    // 4. subscribe: Solo recibe el mensaje final de alerta listo para imprimir.
    .subscribe(
      msg => println(msg),
      e => println(s"Error: ${e.getMessage}"),
      () => println("Monitoring stopped")
    )

  Thread.sleep(12000)
}