Nel lavoro come nella vita, un programmatore può lavorare per semplificarsi il futuro oppure per complicarselo.
Chiaramente, per quanto riguarda la vita privata, questo non riguarda solo i programmatori… ma questa è un'altra storia, che esula dallo scopo di questo articolo.
Uno dei modi in cui un programmatore può complicarsi la vita in futuro è senz'altro quello di costruire un software le cui componenti siano molto interdipendenti le une dalle altre. Di contro, uno dei modi per evitare questa complicazione, e semplificare la vita a sé stessi e ai propri colleghi, consiste nel definire una serie di principi di separazione, cioè delle convenzioni che impediscano ad un elemento software (variabile, funzione, classe, libreria…) di essere contemporaneamente più cose diverse tra loro.
Credo che quello tra domanda (cioè la richiesta di un'informazione) e comando (cioè l'impartizione di un ordine) sia un po' il capostipite di tutti i principi di separazione, ma non mi affretto a trattarlo in sé per sé; invece, come sempre, ci vorrei arrivare attraverso una riflessione.
Cosa significa disaccoppiamento
La riproduzione tra esseri viventi qui non c'entra: nella programmazione, quando si parla di accoppiamento (coupling) si intende un forte legame di interdipendenza tra due diversi elementi di un software, in cui non è chiaramente stabilita la frontiera tra le due rispettive aree di competenza.
Allo stesso modo, per disaccoppiamento, si intende proprio l'assenza di questi legami incrociati di dipendenza tra componenti; a tutti gli effetti non è nemmeno importante che la cosa riguardi coppie di componenti, dacché in molti software si assiste a veri e propri capolavori di promiscuità, cioè insiemi di ben più di due componenti, che condividono e mescolano competenze e informazioni senza nemmeno dare un'idea di quali dovrebbero essere i confini di competenza tra gli uni e gli altri.
Perché è importante disaccoppiare
Il vantaggio di progettare e implementare componenti ben disaccoppiate è evidente: un componente che non basa il suo comportamento sulle informazioni provenienti da un altro componente può essere compreso, utilizzato, mantenuto, modificato o esteso senza dover conoscere o modificare a cascata altre aree del nostro progetto.
Quella del disaccoppiamento in definitiva è semplicemente una basilare pratica di ordine, e di principi di separazione o design pattern che ci aiutano a mantenere quest'ordine ce ne sono veramente tanti.
Partiamo da un principio così fondamentale che è sostanzialmente insito quasi ogni linguaggio di programmazione.
Istruzione vs espressione
Anche se non ce ne accorgiamo, sin dai primissimi passi mossi nel mondo del coding, praticamente ogni riga di codice che scriviamo rientra in una di queste quattro categorie; faremo in modo, ove possibile, di farle ricadere tutte entro le prime due:
-
Espressione
-
Istruzione
-
Dichiarazione/definizione
-
Costrutto
Espressioni
Sono espressioni tutte le scritture che rappresentano un valore: stringhe, numeri, istanze di classi… per capirci, qualsiasi cosa possa essere messo in una variabile o passato come argomento a una funzione è un'espressione. Le espressioni sono il fondamento di ogni linguaggio di programmazione; alcuni linguaggi di programmazione, fortemente orientati verso il pensiero matematico e la logica del calcolo, riducono qualsiasi sintassi del linguaggio ad un'espressione. In qualche modo e un po' grossolanamente (parere non richiesto), anche JavaScript lo fa.
Ragionando in astratto, possiamo pensare ogni espressione come il risultato di un calcolo, più o meno complesso:
Istruzioni
Sono istruzioni tutte le scritture mediante le quali indichiamo alla macchina di fare qualcosa; spesso ma non sempre, le istruzioni coinvolgono a loro volta espressioni:
In alcuni linguaggi (tra cui JavaScript) tutte le istruzioni sono anche espressioni:
Dichiarazioni
Sono dichiarazioni tutte quelle affermazioni mediante le quali dichiariamo (per l'appunto) l'esistenza di un certo elemento del codice (una variabile, una funzione, una classe, etc…).
In JavaScript, alcune dichiarazioni sono anche espressioni:
A tutti gli effetti, in JavaScript le dichiarazioni di una classe o di una funzione sono zucchero sintattico (cioè sintassi che sintetizzano altre sintassi più verbose) e rappresentano una dichiarazione di variabile più l'assegnazione di una classe o funzione (espressioni!) come valore di quella variabile; tali variabili create implicitamente possono anche essere riassegnate:
Il fatto che le parole chiave di una funzione o classe siano mutabili non vuol dire che sia raccomandabile mutarle. A meno che tu non sappia cosa stai facendo, non riassegnare tali variabili.
Sono invece vere e proprie dichiarazioni quelle effettuate mediante var, let, const. Possiamo considerare queste dichiarazioni come un tipo particolare di istruzione.
Costrutti
Sono costrutti tutte quelle scritture sintattiche che il linguaggio mette a disposizione per ramificare e governare l'esecuzione del codice: if, for, while, etc…
In base al fatto che restituiscano o meno un valore, possiamo pensare alcuni costrutti come istruzioni, altri come espressioni:
In altri linguaggi i vari costrutti possono comportarsi come istruzioni o espressioni, in base alle sintassi fornite. In C# sono da poco state implementate le switch expressions; in Rust, che si definisce un linguaggio expression-oriented, praticamente qualsiasi costrutto rappresenta un calcolo che può restituire un valore.
Spero di avervi convinti che praticamente tutto in un sorgente può essere considerato, almeno grossolanamente, come un'istruzione o un'espressione; abbiamo anche visto che le due categorie non sono necessariamente mutualmente esclusive.
Per sintetizzare possiamo dire che le espressioni elaborano un risultato, mentre le istruzioni causano un cambiamento, o side effect.
Che cos'è un side effect
In informatica si considera un side effect qualsiasi cambiamento che un'istruzione produce all'interno o all'esterno del programma.
In particolare, sono side effect:
-
Gli aggiornamenti della UI
-
Le scritture in console
-
Le operazioni su database
-
Le chiamate ad altri processi o servizi
-
Le operazioni su File System
Sono anche side effect cambiamenti apportati internamente allo stato del programma, come le assegnazioni o le modifiche a strutture dati varie ed eventuali.
Sicuramente gli effetti che si propagano all'esterno del programma sono i più interessanti, visto che di fatto sono ciò per cui i clienti ci pagano.
Proprio per questo motivo è così utile separarli dal resto delle nostre elaborazioni.
La command-query separation
Abbiamo detto che non necessariamente c'è una mutua esclusione tra espressioni e istruzioni; allo stesso modo, non esiste una distinzione ferrea tra una funzione che fa qualcosa e una che fornisce qualcosa, qualsiasi funzione può provocare effetti e poi restituire un risultato.
Il principio di separazione domanda/comando non vuole nient'altro che questo: rendere esclusiva la categorizzazione tra istruzione ed espressione, cioè fare in modo che se una funzione dà un risultato, non fa modifiche, e vice versa. In alcuni linguaggi questa separazione è indicata a livello sintattico, ad esempio distinguendo tra procedure (se fanno qualcosa) e funzioni (se restituiscono un risultato).
Nei linguaggi funzionali molto rigorosi, un side effect viene rappresentato come un'espressione da dare in pasto ad un costrutto specifico (come il do statement in Haskell), che è l'unica sintassi del codice autorizzata a produrre effetti. In linguaggi più popolari (e quindi per loro natura più flessibili) questo rigore non è insito nella sintassi e quindi siamo noi programmatori a doverci autoimporre questa limitazione.
Nel rispetto della CQS molti programmatori dividono convenzionalmente le funzioni in funzioni get/set, oppure evitano di proposito di utilizzare le assegnazioni di variabile come espressioni, rinunciando quindi alla possibilità di concatenarle, o di passarle contemporaneamente come argomento. Molti accorgimenti possono essere presi nel rispetto di questo semplice principio.
A questo punto, i vantaggi li abbiamo già individuati. Vediamo un po' meglio in che modo questo principio influenza le nostre architetture nel mondo reale.
La CQS nelle WebAPI
In una web app il rapporto tra front-end e back-end presenta elementi di CQS se implementa correttamente i principali verbi (o metodi) HTTP: get, post, put, delete
, che possiamo brutalmente mappare alle sintassi SQL select, insert, update, delete.
Mentre nel SQL la separazione è totale, visto che l'unica sintassi che fornisce informazioni è la select
, nel caso dell'HTTP dobbiamo venire incontro all'esigenza del frontend di ricevere risposte esaustive, anche dopo aver impartito un comando. Quindi ci troveremo alcune piccole eccezioni.
Get (SQL select)
In HTTP, una chiamata GET rappresenta la richiesta di una risorsa, cioè di un dato, quindi si tratta senza dubbio di una query. Nel rispetto della CQS, nessun endpoint di tipo get dovrebbe produrre dei cambiamenti, né sulla risorsa richiesta né su altre componenti dello stato della nostra applicazione.
Questo comporta alcuni evidenti vantaggi: per cominciare una chiamata get può essere ripetuta infinite volte senza alcun timore, e soprattutto senza preoccuparsi del risultato; come conseguenza, si ha la certezza che la stessa chiamata get darà lo stesso risultato, a meno che in mezzo tra due chiamate non sia stato inviato un comando.
Ovviamente, questo non ci vieta di loggare le chiamate get ricevute sul nostro backend, poiché in questo caso stiamo producendo un side effect di tipo molto particolare. Mi raccomando, loggate anche le query senza esitare se lo ritenete utile per fini diagnostici!
Post (SQL insert)
La chiamata POST è sicuramente un comando, e dal punto di vista della CQS presenta una lieve anomalia. Infatti, molto spesso alcuni campi (tra cui la chiave primaria) vengono generati in fase di insert
, e pertanto è prassi che le chiamate POST facciano qualcosa di simile a quanto segue:
-
Eseguire il comando ricevuto (command);
-
Restituire il risultato dell'endpoint GET della risorsa appena creata (query).
Questa è chiaramente una violazione della CQS, ma viene incontro ad un'esigenza ben specifica: la chiamata POST riceve una risorsa priva di un identificativo univoco (che viene assegnato al momento della insert
su db), quindi ogni POST sta creando un nuovo elemento con una diversa chiave, anche se in realtà si tratta sempre della stessa chiamata ripetuta. Il rimando alla relativa GET è quindi indispensabile per distinguere due o più POST, che potrebbero differire tra loro esclusivamente per la chiave primaria delle risorse create.
Se volessimo essere duri e puri, potremmo evitare tutto questo e rendere la nostra chiamata POST un comando puro e anche ripetibile all'infinito. In alcune web app il frontend si occupa di inizializzare tutti i campi di una risorsa prima di fare la POST. Per quanto riguarda la chiave primaria, si pone un problema: se la nostra chiave è un intero sequenziale, come può il nostro frontend conoscere il giusto valore da inserire?
Elementare, Watson! In questi casi, al posto di un sequenziale, si sceglie come chiave primaria un identificativo di tipo diverso, come i GUID o UUID (globally/universally unique identifier), cioè una stringa generata in modo tale che l'eventualità di una chiave duplicata sia sostanzialmente nulla; per capirci, stiamo parlando di un numero con 38 zeri di possibili GUID.
Questo tipo di chiavi possono tranquillamente essere generate dal frontend prima della POST e sono particolarmente adatte in architetture che presentano database distribuiti.
Questo approccio è generalmente ritenuto più resiliente, perché è trasparente rispetto alle chiamate duplicate e non comporta il rischio di generare risorse duplicate erroneamente, ma questo non vuol dire che sia preferibile. Una politica di gestione stretta degli errori può aiutarci a diagnosticare eventuali problemi, e quindi essere preferibile in tal senso. Si tratta di scelte politiche.
Put (SQL update)
La chiamata PUT è un comando puro e non fa altro che indicare al backend di sostituire una risorsa con la sua versione aggiornata, passata dal frontend. Questo significa che questo verbo presenta gli stessi vantaggi del verbo GET, infatti può essere ripetuto infinite volte senza che il suo effetto cambi.
Nota: per standard, la chiamata PUT può essere usata anche per creare una risorsa, al posto della POST. Questo proprio perché quello che ci si aspetta da una PUT è che dopo la chiamata la situazione sia esattamente quella inviata dal frontend al backend senza nessun tipo di sorpresa. Ad ogni modo questa pratica è abbastanza sconsigliabile.
In alcune implementazioni, la PUT restituisce 404 - Not Found
se si richiede di aggiornare una risorsa inesistente. Questo approccio dipende sempre dalla scelta politica che si compie nei confronti della gestione di chiamate duplicate e altri errori.
Delete (SQL delete)
La chiamata DELETE è particolare, logicamente parlando presenta tutte le caratteristiche di un comando puro e anche di un comando ripetibile all'infinito senza problemi (perché eliminare qualcosa che non c'è significa semplicemente non fare nulla, ma il risultato alla fine è comunque quello richiesto). Tuttavia, sempre per andare incontro al frontend, di norma la chiamata DELETE può restituire come risultato la risorsa appena eliminata, e ritornare un errore 404 - Not Found
qualora si richieda di eliminare una risorsa già rimossa.
Prima di salutarci, vorrei chiudere con un'ultima osservazione. Abbiamo parlato di CQS e poi delle cosiddette operazioni CRUD (create, read/retrieve, update, delete) e in questo passaggio da 2 a 4 casi si è aggiunto un altro elemento di complessità: la ripetibilità o meno di un'operazione, cioè l'idempotenza.
Che cos'è l'idempotenza
L'idempotenza è la caratteristica di un'operazione di essere invariante rispetto al numero di volte che viene eseguita.
Whaaat?
Guarda un po' qui:
In altre parole, dati i due risultati di una funzione applicata 1 volta e della stessa applicata n volte, una funzione è idempotente se i due risultati sono uguali.
-
Il verbo GET è sempre idempotente. Qualsiasi query pura è sempre idempotente.
-
Il verbo POST nella sua versione classica non può essere idempotente. Può diventarlo se il client inserisce tutti i dati (inclusa la chiave primaria) della risorsa da creare e il server accetta la richiesta di creare una risorsa già esistente senza dare errore.
-
I verbi PUT e DELETE sono idempotenti, al netto della gestione degli errori.
Alcune architetture client/server tagliano corto e seguono la seguente prassi:
-
Query -> GET;
-
Command -> POST;
-
ebbasta.
In fin dei conti questa è una semplificazione sempre accettabile, ma richiede la creazione di tre o quattro endpoint di tipo POST con diversi URL, il ché può risultare fuorviante al primo impatto.
Avvertenze
Al di là delle chiamate HTTP, al di là delle istruzioni ed espressioni insite nella sintassi stessa del linguaggio, quando implementi qualsiasi cosa dovresti sempre chiederti se l'API che stai creando crea aspettative corrette o meno in chi la utilizzerà. Il modo più semplice di non tradire le aspettative, è fare una sola cosa alla volta e cioè quella che l'utilizzatore si aspetta.
Se adotti un approccio immutabile, puoi fare persino in modo di avere moltissime espressioni e pochissime istruzioni. Se ad esempio alla richiesta di modificare un oggetto, anziché mutare lo stesso tu scegliessi di generarne uno nuovo a partire dal precedente ma recante le modifiche richieste, avresti sostituito un'istruzione con un'espressione:
Chiaramente, in questo caso sta a te decidere che approccio adottare. Ma sappi che ci sono istruzioni che proprio non potrai evitare: in tal caso, puoi imparare qualcosa da Haskell e marginalizzare le istruzioni, collocandole in aree ben precise del codice, in cui ci si aspetta che i calcoli finiscano e che gli effetti vengano applicati.