Sommario
< Home
Stampa

Comunicazione tra componenti

Nel momento in cui si sviluppa una SPA tramite la realizzazione di pagine a loro volta composte da componenti, si pone un problema di progettazione importante, ovvero come notificare agli altri componenti degli aggiornamenti di stato, ad esempio dovuti ad una modifica dei dati o ad aggiornamenti dovuti all’interazione con l’utente e comunque in generale qualsiasi nuovo evento.

Il problema dell’aggiornamento dei dati

Prendiamo come esempio una applicazione SPA composta da due pagine:

  • una pagina pubblica, che mostra una tabella con informazioni anagrafiche (ad esempio un elenco di impiegati di una azienda), con un componente che chiameremo “public”.
  • una pagina di amministrazione, che consiste in un form dove vengono inseriti/modificati questi dati, con un componente che chiameremo “admin”.

Come abbiamo già visto ciascuna pagina è gestita da uno o più componenti che gestiscono l’interazione con l’utente. Tuttavia ciascun componente non solo è un oggetto incapsulato e quindi con metodi e proprietà non accessibili dagli altri componenti, ma non deve nemmeno essere consapevole dell’esistenza degli altri componenti. Nell’esempio sopra riportato quindi il componente di amministrazione gestisce la modifica dei dati (ad esempio l’aggiunta di una anagrafica) ma non avrebbe modo di comunicare al componente tabella della pagina pubblica che è presente una nuova anagrafica.

Perché è assolutamente necessario che i componenti non comunichino direttamente? Perché se fosse possibile, vorrebbe dire che il componente “admin” dipende direttamente da quello che fa il componente “public”. Questo significa che se in uno sviluppo futuro volessimo modificare il componente public, dovremmo modificare anche il componente admin. Questo significa che il componente admin dipende forzatamente dal modo in cui è implementato il componente public.

Inoltre se volessimo creare un nuovo componente, che mostri per esempio quante sono le anagrafiche, anche questo componente dovrebbe essere aggiornato dal componente admin ogni volta che viene aggiunta una anagrafica, e quindi il programmatore si troverebbe di fronte al problema di dover modificare ancora una volta l’admin, per renderlo “consapevole” del dover far aggiornare gli altri componenti.

La soluzione

La soluzione consiste sostanzialmente nel rendere possibile un aggiornamento da parte di un componente verso gli altri, senza che però questo componente sappia chi sono.

In questa direzione sono state proposte storicamente tre diverse soluzioni:

  • il delegate pattern: in questo caso il componente che vuole essere notificato si registra (ad esempio con una callback detta delegate) presso il componente che sta aggiornando i dati. Il componente che aggiorna i dati chiama il delegate che a sua volta aggiorna i dati;
  • l’observer pattern: si crea una coppia di oggetti, detti Soggetto e Osservatore (Subject e Observer) il primo che contiene l’oggetto da osservare (i dati) e il secondo che osserva le modifiche sui dati. Ogni volta che il soggetto viene modificato, questo notifica all’osservatore che c’è stata una modifica, e questo si comporterà di conseguenza;
  • il publisher-subscribe pattern: in questo caso si crea un oggetto mediatore detto pubsub, visibile a entrambe le parti (generatore di eventi e sottoscrittore di eventi). Il sottoscrittore (ad esempio public) si registra al pubsub con una callback associata ad un determinato evento, mentre il generatore quando si modificano i dati chiama il pubsub emettendo l’evento stesso. Il pubsub quindi richiama tutte le callback dei sottoscrittori notificando loro che c’è un evento.

I primi due pattern sono in generale utilizzati nei linguaggi di programmazione ad oggetti che prevedono il concetto di interfaccia. Tuttavia questo concetto non è presente in tutti i linguaggi (come ad esempio Javascript o Python) e quindi la soluzione più semplice diventa il pubsub pattern.

Vediamo quindi con un esempio come funziona il pubsub.

Pubsub

Il pubsub è molto semplice da implementare in Javascript, ad esempio col codice seguente.

const createPubSub = () => {
    const dict = {};
    return {
        subscribe: (eventName, callback) => {
            if (!dict[eventName]) {
                dict[eventName] = [];
            }
            dict[eventName].push(callback);
        },
        publish: (eventName) => {
            dict[eventName].forEach((callback) => callback());
        }
    }
}

Come si può vedere gli eventi sono gestiti come un dizionario di callback, dove ad ogni chiave corrisponde un evento.

Ipotizziamo di volerlo usare in una semplice applicazione con due componenti: uno che mostra un contatore di click, ed un secondo che contiene due pulsanti, uno che incrementa i click ed uno che azzera il contatore.

Qui l’HTML:

<!DOCTYPE html>
<html lang="en">

<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
      type="text/css" />
   <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
   <title>PubSub</title>
</head>

<body>
   <div id="container" class="container">
      <div id="component1" class="row">
         
      </div>
      <div id="component2" class="row">
        <div class="col">
            <button id="increment" type="button" class="btn btn-positive">Incrementa</button>
        </div>
        <div class="col">
            <button id="clear" type="button" class="btn btn-positive">Azzera</button>
        </div>
      </div>
   </div>
   <script type="module" src="./index.js"></script>

</body>

</html>

Qui il componente 1 (che mostra il contatore):

const createComponent1 = (element, pubsub) => {
    let counter = 0;
    const render = () => {
        element.innerText = "Hai cliccato il pulsante " + counter + " volte.";
    }
    pubsub.subscribe("increment", () => {
        counter++;
        render();
    });
    pubsub.subscribe("clear", () => {
        counter = 0;
        render();
    });
    render();
}

Qui il componente 2 (che gestisce i pulsanti):

const createComponent2 = (element, pubsub) => {
    element.querySelector("#increment").onclick = () => pubsub.publish("increment");
    element.querySelector("#clear").onclick = () => pubsub.publish("clear");
}

A questo punto è sufficiente collegare tutti i componenti al pubsub:

const pubsub = createPubSub();
const component1 = createComponent1(document.querySelector("#component1"), pubsub);
const component2 = createComponent2(document.querySelector("#component2"), pubsub);

Concludendo:

  • pubsub è un componente generico: non sa chi notifica, cosa notifica, svolge quindi il semplice ruolo di passaggio di informazioni.
  • con pubsub i componenti che emettono eventi e quelli che li ricevono non sono consapevoli gli uni degli altri. E’ quindi possibile scrivere componenti in piena libertà senza dipendere dagli altri. Ad esempio si potrebbe scrivere un nuovo componente che mostra l’orario dell’ultimo cambiamento del contatore (si sottoscrive a tutti gli eventi ed aggiorna la data ora).
  • è possibile avere un numero illimitato di eventi, sottoscrittori e in qualsiasi combinazione.

Versioni evolute

Il problema di una gestione centralizzata dei dati unita a quello della notifica ai sottoscrittori ha portato allo sviluppo di soluzioni ancora più evolute di queste. Presentiamo qui il Flux pattern e RXJS, che sono alla base dei framework rispettivamente di React (con Redux) anche se usato anche da Angular, e di RXJS, una libreria fondamentale del framework Angular (ma che può essere usata in modo indipendente).

Flux (e Redux)

In Flux (e la sua principale e più nota implementazione, la libreria Redux) l’obiettivo è realizzare un contenitore generale per tutti i dati e contemporaneamente gestire l’accesso in scrittura e lettura e le notifiche per tutti i componenti. In pratica Flux è sia contenitore dei dati che una specie di Pubsub. In Flux sono previsti 3 concetti:

  • lo store, un oggetto che a sua volta contiene tutti i dati;
  • le views: sono delle funzioni pubbliche dello store che permettono di esportare i dati (ad esempio con una query, un filtro, o altro);
  • le actions: sono delle funzioni pubbliche che permettono di modificare lo store, sono suddivise per tipo (il tipo di modifica) e ricevono come parametri i dati da modificare (o cancellare o aggiungere). Le actions producono una nuova istanza dello store che contiene le modifiche richieste (ed il vecchio store viene eliminato).
  • I dispatcher: i dispatcher sono i publisher del pubsub, emettono eventi ai sottoscrittori, che vengono quindi notificati che lo store è cambiato.

Con questo sistema si separa in modo netto il flusso di modifica dello store (ad esempio inserendo un nuovo dato) dallo store stesso (che ha internamente le procedure per modificare il dato), così come si separa la notifica dalla visualizzazione dei dati. Questo principio si chiama CQSP (Command Query Segregation Principle), ovvero separa il flusso di modifica da quello di visualizzazione, che è un po’ il principio di fondo che abbiamo visto all’inizio della lezione.

Flux introduce concetti di programmazione funzionale (lo store è immutabile, ad ogni modifica ne viene creato uno nuovo, ed ogni trasformazione è eseguita da una funzione) ed è alla base del framework React (con Redux) ma è usatissimo anche in Angular (NgRx) e Vue (Vuex) e rappresenta la vera svolta nelle SPA perché permette di gestire un vero e proprio domain model.

RXJS

La libreria RX ha un approccio completamente nuovo al problema, in quanto utilizzo un altro aspetto della programmazione funzionale, ovvero il concetto di funzione che esegue un insieme di trasformazioni parziali su un set di dati. RXJS unisce questo flusso al concetto di promise.

Alla base di RX c’è l’oggetto Observable, che a partire da un set di dati, crea un oggetto concettualmente identico ad una promise, che però a differenza della promise può essere elaborato da una operazione di trasformazione, che produce quindi un nuovo Observable. In pratica quindi è possibile collegare in sequenza le azioni in una catena (chiamata pipe) che al termine producono un nuovo Observable.

L’Observable è come una promise, la serie di azioni è pronta per essere eseguita. Per attivarlo (e quindi eseguire tutta la sequenza) c’è quindi una azione subscribe che riceve come argomento un oggetto Observer, che riceve il risultato finale e lo elabora per la sua destinazione finale (ad esempio una render, oppure una fetch).

Inoltre, siccome, gli Observable sono oggetti, possono essere parzialmente elaborati (ad esempio applicando trasformazioni come il map o reduce) da componenti distinti e produrre quindi degli Observable “semilavorati” che possono poi essere successivamente passati al componente successivo per una successiva elaborazione o al componente finale.

L’aspetto più interessante è poi quello di rendere possibile la costruzione dinamica della pipe in base a condizioni dinamiche (quindi a runtime), il fatto di poter collegare la stessa fonte di dati a più Observers, o viceversa combinare più Observable in uno solo.

RXJS è quindi qualcosa di più che una libreria che aiuta a gestire i dati, ma un nuovo modo di concepire il loro flusso. Un Observable può essere sia una fonte di dati, ma anche un evento utente, o una fetch remota, un timeout, o un interval (che emette periodicamente un evento).