Come la programmazione object-oriented, la programmazione funzionale e la programmazione procedurale, il termine Reactive Programming identifica un paradigma di programmazione relativamente nuovo. In internet si trovano diverse definizione di programmazione reattiva, anche molto diverse tra loro, alcune delle quali sono riportate nella tabella seguente (con le relative fonti).
FONTE | DEFINIZIONE |
Microsoft | Reactive Extensions is for composing asynchronous and event-based programs using observable sequences and LINQ-style query operators. |
Wikipedia | Reactive Programming is a programming paradigm oriented around data flows and the propagation of change. |
Reactive Manifesto | Reactive Systems are Responsive, Elastic, Resilient, Message Driven. |
La definizione fornita da Microsoft genera confusione, mentre quella di Wikepediaè generica ed eccessivamente teorica. Da una loro attenta analisi è però possibile ricavare quali sono i concetti che sono alla base dela programmazione reattiva:
- E’ focalizzata sul concetto di flussi di dati e sulla propagazione dei cambiamenti.
- Permette di gestire facilmente flussi asincroni di dati ed eventi (asynchronous streams).
- Fortemente correlata alla programmazione ad eventi.
La programmazione reattiva, quindi, ben si presta alla realizzazione di moderne applicazioni di tipo data-drive, ovvero di quelle applicazioni in cui il flusso delle operazioni è fortemente dipendente dai dati che tratta. In queste sistemi il set di dati di input può modificare il normale comportamento dell’applicazione.
L’introduzione della programmazione reattiva costituisce un tentativo di creare più facilmente applicazioni altamente interattive, a bassa latenza e in costante aggiornamento. Prima di parlare di come tale paradigma differisca dai paradigmi di programmazione precedenti, descriviamoli brevemente.
Paradigma di Programmazione Sincrona
Il paradigma di programmazione sincrono è quello con cui la maggior parte degli sviluppatori ha familiarità. La ragione è che, oltre ad essere il più semplice, è anche un ottimo paradigma con cui avviare qualsiasi applicazione. In questo paradigma, gli eventi si susseguono in modo sequenziale, ogni evento viene eseguito in ordine e non possono verificarsi due eventi contemporaneamente.
Ciò che rende il paradigma sincrono così potente è che è davvero facile seguire e capire cosa sta facendo un’applicazione. E’ possibile guardare attraverso tutte le linee di codice e comprendere cosa ha eseguito e cosa ha fatto. Conseguentemente quando si verificano errori, spesso sono facili da trovare e ancora più facili da risolvere.
Paradigma di Programmazione Asincrona
Man mano che un’applicazione cresce, la user experience diventa più complessa e l’elaborazione dei dati più intensa, gli sviluppatori sono costretti a cercare soluzioni più efficienti ai loro problemi. Una di queste è la programmazione asincrona. Nel paradigma asincrono, due eventi possono verificarsi nello stesso momento o in un ordine qualsiasi, in modo imprevedibile.
L’uso di tale paradigma ha migliorato significativamente sia l’esperienza utente che le performance di molte applicazioni. Un utente infatti, può interagire con una maschera o form, mentre un thread in background la aggiorna, senza interrompere il flusso di lavoro dell’utente. Oppure un batch può elaborazione dei dati dividendoli in porzioni lavorabili contemporaneamente, ottenendo un’applicazione molto più veloce.
Tutto ciò comporta comunque dei compromessi. L’uso della programmazione asincrona rende un programma molto più difficile da leggere e da debuggare, soprattutto quando si lavora in un team. Inoltre introduce problematiche che prima non avevamo, come l’accesso concorrente ai dati, il deadlock dei thread e le race condition.
Paradigma di Programmazione Reattiva
Con l’uso intensivo dell’elaborazione asincrona, le applicazioni si avvicinano sempre di più alla programmazione reattiva. In un certo senso la programmazione reattiva può essere vista come un sottoinsieme dell’elaborazione asincrona che tratta i dati in un modo molto speciale.
La programmazione reattiva si concentra sui dati e su come cambiano nel tempo. Invece di pensare il codice come ad un agente che guida i cambiamenti in un’applicazione, nella programmazione reattiva sono i dati l’agente del cambiamento. L’unica responsabilità del codice è di rimanere in ascolto sui cambiamenti dei dati e reagire facendo qualcosa in risposta a tali cambiamenti.
Il motivo per cui questo paradigma è un modo utile di risolvere problemi complessi nell’ingegneria del software è che ci si libera dell’illusione del controllo. Scrivendo metodi in modo tale da non avere alcun controllo sulla provenienza dei dati, evitiamo di costruire delle (fragili) dipendenze tra i metodi. Senza tali dipendenze è possibile creare applicazioni che sono molto più pronte per essere eseguite in modo asincrono.
La programmazione reattiva non deve però essere considerata come la soluzione a tutti i mali. Innanzitutto è applicabile solamente quando si dispone di set di dati di grandi dimensioni, o di set di dati che cambiano costantemente o di serie di dati infiniti (come gli eventi di clic dell’utente in un’applicazione). Inoltre è ancora molto più difficile da comprendere rispetto alle sue controparti sincrone.
Reactive Programming vs. Reactive Streams
Dopo aver chiarito cosa si intende per programmazione reattiva in generale, torniamo al mondo java e vediamo come questa è declinata in tale linguaggio. Per farlo dobbiamo parlare della specifica Reactive Stream. Tale specifica definisce un set comune di API per la programmazione reattiva in java. La definizione che troviamo nella pagina ufficiale dell’iniziativa è la seguente:
Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure.
il che significa che l’intento della specifica è di fornire uno standard per l’elaborazione asincrona di dati organizzati in uno stream, fornendo un meccanismo per la gestione del backpressure in modo non bloccante. Per comprendere tale definizione dobbiamo innanzitutto descrivere alcuni concetti che sono presenti in una architettura di stream processing generalizzata. Prendendo a riferimento la figura seguente, gli elementi distintivi sono:
- il produttore (PRODUCER) ovvero la sorgente dei dati;
- il consumatore (CONSUMER) ovvero a chi sono destinati i dati;
- una o più fasi di processamento (PROCESSING) che eseguono operazioni si dati.
In tale modello esiste quindi un flusso naturale dei dati dal produttore al consumatore. Cosa accade però nel momento in cui le varie componenti lavorano a differenti velocità di processamento? Sostanzialmente due sono gli scenari da considerare:
- Se il componente a valle, ovvero quello che riceve i dati, è più veloce di quello a monte, che li invia, tutto va bene, e la catena funzionare senza intoppi.
- Se, invece, il componente a monte è più veloce a produrre i dati di quanto non lo sia il componente a valle ad elaborarli, allora le cose iniziano a peggiorare.
In questo secondo caso, che è evidentemente quello che va gestito, esistono diverse strategie che possiamo utilizzare per gestire i dati in eccesso:
- Bufferizzarli, ma in genere i buffer hanno una capacità limitata e prima o poi esauriscono la loro memoria.
- Scartarli, comportando una perdita di dati che può essere accettabile in alcuni casi ma assolutamente da evitare in altri.
- Bloccarli fino a quando il consumatore ha finito l’elaborazione, ma questo potrebbe comportare il rallentamento dell’intera catena.
Il modo migliore per gestire la diversa capacità di elaborazione consiste nell’utilizzo di una tecnica chiamata backpressure, che consiste nel dare la possibilità al consumatore (più lento) di richiede al produttore (più veloce) la quantità di dati che è in grado di elaborare in quel momento. Tornando al diagramma mostrato nella figura precedente, si può pensare alla backpressure come ad un tipo particolare di segnale, che scorre nella direzione opposta rispetto ai dati che vengono elaborati.
Reactive Streams API
Per concludere diamo una rapida occhiata alle API definite nella specifica Reactive Streams. Esse consistono molto semplicemente in 4 interfacce.
Publisher
Il publisher è un produttore di un numero, potenzialmente illimitato, di elementi sequenziati, che vengono forniti in base alle richieste ricevuta dai suoi sottoscrittori.
1 2 3 |
public interface Publisher<T> { public void subscribe(Subscriber<? super T> s); } |
Subscriber
Il subscriber è il componente che riceve i dati dal publisher. Per farlo deve effettuare una sottoscrizione ed ottenere un oggetto di tipo Subscription
.
1 2 3 4 5 6 |
public interface Subscriber<T> { public void onSubscribe(Subscription s); public void onNext(T t); public void onError(Throwable t); public void onComplete(); } |
Subscription
E’ l’oggetto attraverso il quale è gestita la sottoscrizione, ed è attraverso il suo metodo request()
che si realizza il meccanismo di backpressure.
1 2 3 4 |
public interface Subscription { public void request(long n); public void cancel(); } |
Processor
Un processor rappresenta una fase di elaborazione dello stream di dati ed è sia un subscriber che un publisher, e quindi obbedisce alle specifiche di entrambe.
1 2 |
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> { } |
Reactive Streams Implementazioni in Java
Il panorama della programmazione reattiva in Java è in continua evoluzione e maturazione. David Karnok ha un ottimo post sul blog Advanced Reactive Java, in cui descrive diverse generazioni di progetti reattivi. A completamento dell’articolo, nella tabella seguente ne elenchiamo qualcuono:
RxJava | E’ l’implementazione Java del progetto ReactiveX, disponibile oltre che per Java anche per i linguaggi JavaScript, .NET (C#), Scala, Clojure, C++, Ruby, Python, PHP, Swift, etc. |
Reactor | E’ una implementazione Pivotal conforme agli standard Reactive Stream. La funzionalità reattiva presente in Spring Framework 5 è basata su Reactor 3.0. |
Ratpack | E’ un set di librerie Java per la creazione di moderne applicazioni HTTP ad alte prestazioni. |
Vert.x | E’ un progetto di Eclipse Foundation. È un framework basato su eventi per la realizzazione di applicazioni per JVM. Il supporto reattivo in Vert.x è simile a Ratpack. Vert.x consente di utilizzare RxJava o la loro implementazione nativa dell’API Reactive Streams. |
Reactive Streams for Java | Dalla release 1.8 di Java è presente un valido supporto per le specifiche Reactive Stream, che però non fa dell’API core della JVM ma è disponibile come jar separato. Le cose sono cambiate con Java 1.9 in quanto Reactive Streams è diventato parte dell’API Java 9 ufficiale. |