CONTATTACI

Blog

Tutto quello che c’è da sapere sugli stati

Quasi ogni sviluppatore web trascorre la prima parte della sua carriera producendo software senza una vera e propria cognizione di cosa sia uno stato e di come gestirlo. Il motivo è, principalmente, che, molto spesso, framework e librerie si prendono cura di questo aspetto per noi. 

Beh, potremmo dire che questo è un bene… lo è, vero? 
La verità sta nel mezzo. Da un lato è sicuramente auspicabile che un framework degno di questo nome si prenda cura di gestire lo stato interno dell'applicazione; d'altra parte è evidente che, se passiamo anni a fare applicazioni senza la minima nozione di cosa sia una macchina a stati, una riduzione o un sistema di change detection, probabilmente non stiamo facendo molti progressi e sicuramente finiremo col generare complessità inutili, proprio perché la gestione dei nostri stati mancherà di una struttura architetturale consapevole. 

Iniziamo dunque, come sempre, dalle definizioni: 

Che cos'è uno stato? 

Lo stato di un'applicazione è la condizione in cui questa si trova in un dato momento. Perché si parli di stato, le informazioni devono essere disponibili in modo più o meno persistente, ma non totalmente transitorio. 

La domanda sorge spontanea: è necessario che un'applicazione abbia uno stato? 
Assolutamente no, in teoria. In pratica, all'incirca sì. 

Che noia le risposte ambigue, eh? 
Proviamo ad argomentare.

Stateful vs stateless 

A seconda del tipo di applicazione che stiamo sviluppando, seguiremo una definizione, più o meno rigorosa, di stato. Generalmente, le applicazioni stateful sono quelle in cui, in qualsiasi momento, viene messa a disposizione delle logiche una fotografia accessibile dei dati con cui l'applicazione sta lavorando. 

Se ad esempio, nella nostra applicazione, sono presenti una serie di variabili più o meno globali, che vengono manipolate man mano che succedono cose e restano a disposizione nella loro versione più aggiornata, la nostra applicazione è stateful. La definizione va allentandosi se tale stato può essere messo a disposizione su richiesta, ad esempio interrogando un database oppure un servizio esterno. In tal caso, la gestione dello stato è comunque presente, ma è delegata a un componente software esterno, per cui, ad essere rigorosi, dobbiamo considerare la nostra applicazione stateless. 

Volendo fare una brutale banalizzazione della realtà: 

Tipo di applicazione 

Stateful/Stateless 

Backend con database 

Stateless, se si esclude il database 

Backend in-memory 

Stateful 

Frontend client-side 

Generalmente stateful 

Frontend server-side 

Generalmente stateless 

Funzionalità singola (input -> output) 

Stateless, se si escludono input e output (e generalmente si escludono) 

FormaLo stato è, solitamente, definito in contrapposizione alle logiche che lo manipolano; tali logiche sono spesso definite comportamento dell'applicazione. 

Il comportamento 

A questo punto, si pone un problema non da poco, su cui la comunità dei web developer si interroga sin dall'alba dei tempi: in che modo il comportamento dovrebbe leggere e/o modificare lo stato? 
Bisogna dire che dalla risposta che diamo a questa domanda dipendono, concretamente, cose che potremo o non potremo fare e linguaggi di programmazione che potremo o non potremo utilizzare. Come dicevamo, una questione non banale. 

Mutare lo stato 

Se stiamo realizzando un editor di testo, è evidente che una parte importante dello stato della nostra applicazione è costituita dal testo che stiamo modificando. 

Questo esempio, molto sofisticato, deriva dal fatto che, al momento, stiamo scrivendo un articolo utilizzando proprio un editor di testo. 

Dunque, nel momento in cui digitiamo un nuovo carattere, è molto probabile che da qualche parte, nel componente editor della nostra applicazione, ci sia un campo text che viene aggiornato. Se stiamo realizzando un prodotto software un minimo sofisticato, è alquanto improbabile che useremo direttamente il valore del componente grafico che rappresenta l'editor; più probabilmente ci avvarremo di una qualche forma di data-binding, cioè di aggiornamento automatico della proprietà con cui le nostre logiche lavoreranno, ogni volta che l'elemento della UI viene aggiornato. 

Ebbene, quanto appena descritto si definisce un approccio mutable, ed è la soluzione preferita in molte architetture a oggetti: ogni volta che l'utente fa qualcosa, la nostra proprietà editor.text viene aggiornata di conseguenza. Similarmente, se premiamo il tasto grassetto, la proprietà editor.bold verrà assegnata a true, etc…

  • Il vantaggio di questo approccio è che la mappatura tra l'azione dell'utente e lo stato è direttissima e non richiede particolari logiche aggiuntive;
  • Uno svantaggio è che, se dovessimo "incastrare" delle logiche particolarmente complicate nel momento in cui l'utente fa qualcosa, dovremmo tirare su un sofisticato labirinto di eventi e dovremmo, anche, fare attenzione a dove interveniamo: un evento potrebbe scattare prima o dopo che lo stato sia stato aggiornato;
  • Un altro svantaggio è che, se dovessimo mai utilizzare un linguaggio che supporta il calcolo parallelo, potremmo sperimentare problemi legati alla concorrenza, cioè al fatto che diversi thread stanno intervenendo sullo stato, e potrebbero farlo in modo tale da rendere lo stato completamente inconsistente.

Sostituire lo stato 

Un altro approccio, generalmente preferito in paradigmi di programmazione funzionale, è quello per cui, ad ogni evento corrisponde una sostituzione dello stato. Per la precisione, tale approccio è definito riduttivo, perché partendo da uno stato iniziale e aggiungendo una serie di trasformazioni (o transizioni), estrae lo stato finale, cioè riduce un elenco di azioni su uno stato iniziale ad uno stato finale (o meglio, allo stato corrente). 

  • Un vantaggio gigante di questo approccio è il supporto automatico al multi-threading. Qualsiasi componente stia agendo sullo stato produrrà una sua copia aggiornata dello stato. Se poi anche lo stato stesso è rappresentato come un evento, si parla di programmazione reattiva; a questo punto non è possibile per nessun componente conservare lo stato, ma solo trasformarlo e poi informare altri componenti;
  • Un altro vantaggio di questo approccio è che rende possibile centralizzare la gestione di eventi eterogenei: ogni volta che succede qualcosa, una sola funzione (detta riduttore) viene chiamata con lo stato precedente e un parametro che indica "ciò che è successo". In questo modo, tutti gli eventi vengono gestiti come uno solo, recante al suo interno informazioni specifiche sulla transizione che lo stato dovrà subire; 
  • L'elefante nella stanza è un piccolo ma cruciale svantaggio: la memoria. Prendiamo l'esempio di prima ed immaginiamo, ad ogni pressione di un tasto sulla tastiera, di chiamare un riduttore, passando il contenuto del nostro editor più lo stato di tutti i pulsanti (grassetto, corsivo, etc…). Non solo. Ad ogni pressione, il contenuto del nostro stato non viene aggiornato ma completamente sostituito. Immaginate di fare questa cosa non con un editor di testo, ma con la registrazione di un video, è evidente che questo approccio non è sostenibile là dove l'elaborazione richiede un certo livello di ottimizzazione. La realtà dei fatti è che, nelle applicazioni di tutti i giorni, questo elefante in realtà è piccolo piccolo e non costituirà praticamente mai un problema. 

Architetture e design pattern 

Le seguenti architetture o design-pattern hanno principalmente due obiettivi: 

  • separare la presentazione (cioè le viste) dalle logiche; 
  • facilitare un approccio ordinato alla gestione dello stato dell'applicazione.

Model-View-Controller (MVC) 

Questo pattern ha una certa età, ma non passa mai di moda; facilita un approccio orientato agli oggetti ed è composto dai seguenti strati: 

  • Model: rappresenta lo stato dell'applicazione o, per lo meno, di una parte di essa. Generalmente, è un oggetto passivo, con nessuna o pochissime logiche, mappa un database, o comunque è un qualcosa di memorizzabile o serializzabile; 
  • View: questo pattern è, anch'esso, uno strato relativamente passivo, rappresenta i dati e raccoglie le interazioni dell'utente; 
  • Controller: è il detentore del comportamento, genera la vista a seconda dei dati presenti nel model; al presentarsi di un evento, elabora il model (cioè manipola lo stato) e, poi, aggiorna la vista di conseguenza, rigenerandola. 

Questo pattern ha il vantaggio di essere molto semplice. Può essere considerata una basilare forma di separazione tra stato (model), presentazione (view) e logiche (controller), ma tende a sovraccaricare di responsabilità il controller. Generalmente, è adottata in applicazioni in cui l'interazione tra vista e stato avviene in momenti ben scanditi. Un classico esempio è rappresentato dalle applicazioni web renderizzate lato server, dove il pattern MVC è, ad oggi, lo standard. 

Model-View-ViewModel (MVVM) 

Questa è un'architettura relativamente nuova ed è, principalmente, adottata in framework moderni, che prevedono un'interazione più fitta tra vista e modifiche allo stato. Come nell'MVC, vengono individuati tre strati, ma le responsabilità sono un pochino ripartite: 

  • Il model rappresenta sempre lo stato, è sempre un oggetto tendenzialmente passivo ma in questo caso può presentare qualche logica aggiuntiva in merito ad esempio alla manipolazione dei dati che contiene; 
  • Anche la view si assume qualche responsabilità in più, precisamente si preoccupa di creare dei data-binding a doppia via tra la presentazione e lo stato. In altre parole la view si preoccupa di mantenere aggiornato lo stato a seconda di ciò che l'utente fa; 
  • Il ViewModel è letteralmente il Model della View, cioè è un componente (generalmente una classe) che mappa tra ciò che c'è nella vista e quello che è lo stato interno dell'applicazione, eventualmente applicando tutte le logiche di comportamento (validazioni, salvataggi, caricamenti, etc…). 

I framework MVVM presentano una qualche forma di change detection, cioè una strategia per capire quando lo stato dell'applicazione è cambiato e, dunque, la vista dev'essere aggiornata. Una strategia di change detection può essere quella di rigenerare la vista ogni volta che succede qualcosa, controllando se lo stato è cambiato; un'altra strategia molto comune (e decisamente più ottimizzata) è quella dell'observer pattern: i campi dello stato non vengono rappresentati come variabili, ma come flussi di eventi che vengono scatenati ogni volta che il valore viene aggiornato. In questo caso la view sa esattamente quale parte dello stato è cambiata e quindi procede ad aggiornarla. 

MVC vs MVVM 

Potrà esserti sembrato che tra MVC e MVVM cambi veramente poco, e in effetti è così. Le principali differenze sono: 

  • In un caso (MVC) il controller è l'elemento centrale e potrebbe essere responsabile di una o più viste, mentre gli altri strati sono passivi; nell'altro caso (MVVM) la vista assume una sua centralità, eventualmente "stringendo" data-binding con tutti i ViewModel necessari, in base a quante diverse aree dello stato vengono manipolate da tale vista. 
  • In un caso (MVC) la comunicazione tende ad essere ben scaglionata: ad esempio, nel compilare un form, il controller potrebbe non essere mai interpellato fino al momento della sottoscrizione, dopodiché la vista verrebbe completamente ricaricata da zero. Nell'altro caso (MVVM) la vista mantiene aggiornato il ViewModel e quindi anche il Model ad ogni azione dell'utente, e al momento della sottoscrizione non sarà necessario che il ViewModel riceva tutto il contenuto del form, perché ce l'ha già a disposizione, già aggiornato. Decisamente più efficace se si vuole avere un controllo più capillare tra ciò che l'utente fa e ciò che l'applicazione deve recepire ed elaborare. 

Flux/Redux 

Quest'architettura è probabilmente più snella delle due già citate, e non individua in modo rigido una "filiera" di lavorazione dei dati e degli eventi. Invece, si limita a servire lo stesso proposito nel modo più sintetico possibile. Individua i seguenti strati: 

  • Stato: rappresenta ciò che nei due pattern precedenti è il model. Qui lo stato è tendenzialmente un oggetto unico per tutta l'applicazione, internamente ripartito in tutte le componenti funzionali della stessa; lo stato è un oggetto decisamente passivo e non ha alcuna logica integrata; 
  • Azione: rappresenta una "cosa" che può "succedere". In altre parole, un evento, ma non nella forma normalmente intesa in programmazione. Un'azione potrebbe essere un oggetto passivo o una funzione che viene passata come parametro al riduttore; 
  • Riduttore: rappresenta una funzione centrale, che potrebbe per analogia corrispondere al controller che riceve come parametro lo stato iniziale e l'azione che è stata richiesta, e restituisce lo stato finale elaborato secondo le logiche di comportamento. 
  • (Vista): in pattern di questo tipo la vista non è altro che una funzione che mappa lo stato in una presentazione, cioè fa corrispondere ad ogni possibile stato una sua rappresentazione. Di fatto questo strato può essere o meno integrato nel riduttore, o viceversa, il riduttore può essere integrato nella funzione che si occupa di (ri)elaborare la vista. 

Questo pattern a tutti gli effetti è una versione funzionale del pattern MVC. Non essendo orientato agli oggetti non prevede l'uso di classi per rappresentare i diversi strati. Generalmente, l'oggetto è una struttura di dati più o meno manipolabile, l'azione è anch'essa un oggetto e il riduttore è una funzione statica dove di fatto avviene tutta la magia. Se lo chiedete a noi, questo pattern, per quanto vada, oggi, molto di moda, non presenta grosse novità e d'altro canto nemmeno supera il problema di sovraccaricare di responsabilità uno strato rispetto agli altri. Di solito, in progetti grandi sviluppati secondo questo pattern i riduttori sono funzioni di una complessità improponibile. Inoltre, framework che implementano questo pattern (primo tra tutti React), pur presentandosi come snellissimi, hanno il difetto di fallire miserabilmente nel separare la presentazione dalle logiche, e, anzi, probabilmente non si pongono nemmeno tale proposito, dal momento che la vista viene rappresentata direttamente nella funzione del linguaggio JavaScript che elabora le logiche. 

Reactive/A eventi 

Questa non è una vera e propria architettura, ma un approccio che può essere utilizzato in diversi tipi di design. L'approccio a eventi è molto vicino di per sé al già citato observer pattern, diffuso in framework MVVM. A tutti gli effetti, il tutto si può riassumere in tre strati: 

  • Sorgenti: sono tutte quelle cose in grado di far succede qualcosa: un pulsante, una chiamata HTTP, un timer, qualsiasi cosa che abbia il potere di innescare un cambiamento di stato. A proposito: un aspetto particolarmente elegante, qui, è che lo stesso stato iniziale si presenta come un evento scatenato all'avvio; 
  • Pipeline di operatori: sono composizioni di funzioni che combinano gli eventi e ne mappano il contenuto per generare nuovi eventi; di fatto, l'insieme delle stesse potrebbe essere assimilato a un riduttore;
  • (Subscriber): sono funzioni che ascoltano e reagiscono agli eventi che emergono dalle pipeline, aggiornando la vista di conseguenza. Nei framework che implementano questo pattern, di solito questo strato si presenta già implementato nel framework ed è più o meno nascosto al programmatore informatico. In Angular (framework a oggetti) è possibile utilizzare solo flussi di eventi e non usare esplicitamente nemmeno un subscriber. Anche in cycle.js (un framework funzionale) i subscriber (qui definiti driver) sono integrati nel framework e non rappresentano una preoccupazione per il programmatore. 

Personalmente, troviamo molto elegante la soluzione di trattare lo stato come uno degli eventi in gioco, in modo tale che non ci sia mai un'istruzione esplicita di inizializzarlo né di aggiornarlo. Un altro vantaggio è costituito dalle pipeline, che tendono a non centralizzare troppa logica su una sola funzione. L'altra cosa bella di questo approccio è che mette perfettamente d'accordo programmazione funzionale (utilizzata per gestire i flussi di eventi) e programmazione a oggetti (utilizzata per organizzare i componenti e fornire un'api comoda e scorrevole su tali flussi). 

FormaLo stato, da un punto di vista pratico 

Per concludere, vorremmo riflettere un pochino su quanto stato serve nelle applicazioni. Distinguiamo, velocemente, tra lo stato di sessione e lo stato persistente: 

  • Lo stato di sessione muore quando abbiamo finito (oppure se annulliamo la nostra operazione); 
  • Lo stato persistente resta a meno che non sia prevista un'azione esplicita per cancellarlo. 

Qui, al di là di tutte le trattazioni fatte, ci dovremo porre problemi come dove mettere lo stato, per esempio: 

  • in memoria: lo stato morirà quando chiudiamo la web app 
  • nel session storage: è pressoché come metterlo in memoria 
  • nel local storage (o altro store persistente sul browser): qui sopravvivrà alla chiusura dell'applicazione 
  • sul server: in questo modo possiamo centralizzare tutto, ma sono richieste più logiche 

Personalmente concordiamo sul principio che tutto lo "stato evitabile" andrebbe evitato. D'altra parte, in termini di user experience, ci sono situazioni in cui l'applicazione non può dimenticare tutto. 

Forse, piuttosto che non gestirlo, potrebbe essere comodo prendere in considerazione uno strumento (libreria o framework che sia) che ci consenta di farlo con poco poco sforzo. A quel punto, poco importa con quale architettura questo viene fatto; l'importante è capire come lo strumento che adottiamo è pensato, in modo tale da sfruttarlo appieno. 

Articoli correlati

Il linguaggio HTML (Hypertext Markup Language) è uno dei più conosciuti e diffusi: lo si trova infatti praticamente in tutte le pagine web esistenti. È una scelta popolare tra i principianti che si…
Essere uno sviluppatore web vuol dire imparare più linguaggi di programmazione per poter creare diversi tipi di applicazioni.  Per ogni applicazione, la maggior parte delle volte, …
Un buon programmatore, si sa, necessita di un ampio bagaglio di conoscenze per potersi districare tra le difficoltà che questo mestiere comporta. Oggi le richieste sul mercato sono molteplic…
Il mondo del web development è in continua evoluzione e, se da un lato può sembrare una corsa incessante, dall’altro rappresenta senz’altro un vortice di sfide e opportunità.  Come sviluppa…
Il mondo dello sviluppo backend è una parte cruciale di qualsiasi progetto web: definisce la logica che alimenta il lato “non visibile” di un sito o di un’applicazione. Si lega al frontend per mescol…
GitHub è da anni uno degli strumenti di condivisione del codice e versioning più utilizzati tra i web developers. Ad oggi, nel mondo del web development risulta essere uno strumento imprescindibile…

Seguici su Facebook

Vuoi entrare nel mondo della programmazione?

Scopri di più sui nostri corsi!

Chiamaci al numero verde

Contattaci su WhatsApp

Contattaci senza impegno per informazioni sul corso

Pagamento rateale

Valore della rata: A PARTIRE DA 115 €/mese.

Esempio di finanziamento 

Importo finanziato: € 2440 in 24 rate da € 115 – TAN fisso 9,55% TAEG 12,57% – importo totale del credito € 2841.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

*In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Pagamento rateale

Valore della rata: A PARTIRE DA 187 €/mese.

Esempio di finanziamento  

Importo finanziato: € 3990 in 24 rate da € 187 – TAN fisso 9,55% TAEG 12,57% – importo totale del credito € 4572.88.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

* In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Contattaci senza impegno per informazioni sul corso

Scopriamo insieme se i nostri corsi fanno per te. Compila il form e aspetta la chiamata di uno dei nostri consulenti.