Blog

O domandi o comandi – Query-Command Separation e dintorni

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: 

// number expressions 
1 // equals 1 
1 + 1 // equals 2 
Math.floor(56.2 + 11.12 – 1.11) // equals 66
// string expressions 
'a string literal' // equals 'a string literal' 
'a concatenation' + ' of strings' // equals 'a concatenation of strings' 
`a template ${'string'}` // equals 'a template string' 
['h', 'e', 'l', 'l', 'o'].join('') // equals 'hello' 

 

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:

// log 'hello' in console 
console.log('hello!') 
// put some HTML into the body 
document.body.innerHTML = '<h1>Hello!</h1>' 
// open empty alert prompt 
alert()

In alcuni linguaggi (tra cui JavaScript) tutte le istruzioni sono anche espressioni:

// a function that returns nothing, is implicitly returning undefined 
var a = console.log('hello!') // a = undefined 
var b = alert() // b = undefined 
// an assignment implicitly evaluates its content 
var c = document.body.innerHTML = '<h1>Hello!</h1>' // c = '<h1>Hello!</h1>' 

 

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…). 

// class declaration 
class Person { 
    constructor(name, age) { 
        this.name = name 
        this.age = age 
    } 
// function declaration 
function sum(first, second) { 
    return first + second 
// 'var' declaration (and assignment) 
var john = new Person('john doe', 22) 
// 'const' declaration (and assignment) 
const eight = sum(3, 5)

 

In JavaScript, alcune dichiarazioni sono anche espressioni:

// a class declaration is an expression (and can thereby be anonymous) 
var Person = class { 
    constructor(name, age) { 
        this.name = name 
        this.age = age 
    } 
var Person2 = Person 
var john = new Person('john doe', 22) 
var foo = new Person2('foo bar', 42) 
// a function declaration is an expression (and can thereby be anonymous) 
var sum = function(first, second) { 
    return first + second 
var six = sum(4, 2)

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: 

class Person { 
    constructor(name, age) { 
        this.name = name 
        this.age = age 
    } 
Person = 1 
function sum(first, second) { 
    return first + second 
sum = 'hey' 
console.log(Person, sum) // 1 'hey'

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: 

// if, while, for statements are instructions 
if (true) { /*…*/ } 
// ternary operator is an expression 
someExpression ? ifTrue : ifFalse 

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. 

FormaPrima 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: 

// plus zero is idempotent 
function plusZero(num) { 
    return num + 0 
function repeat(func, times, init) { 
    return Array.from({ length: times }).reduce(func, init) 
var oneTime = repeat(plusZero, 1, 4) 
var manyTimes = repeat(plusZero, 6, 4) 
console.log(oneTime) // 4 
console.log(manyTimes) // 4 
// plus one is not idempotent 
function plusOne(num) { 
    return num + 1 
function repeat(func, times, init) { 
    return Array.from({ length: times }).reduce(func, init) 
var oneTime = repeat(plusOne, 1, 4) 
var manyTimes = repeat(plusOne, 6, 4) 
console.log(oneTime) // 5 
console.log(manyTimes) // 10

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. 

Forma 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: 

class Person { 
    constructor(name, age) { 
        this.name = name 
        this.age = age 
    } 
    // immutable approach -> mutation as result 
    makeOlderExpression(years) { 
        return new Person(this.name, this.age + years) 
    } 
    // mutable approach -> mutation as effect 
    makeOlderInstruction(years) { 
        this.age += years 
    } 

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. 

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…
Un fattore importante e spesso sottovalutato per avere successo nel campo del web development è la capacità di fare buone domande e sapere dove trovare aiuto all’occorrenza. Quando Google non bast…
Le tecnologie legate all’intelligenza artificiale stanno facendo progressi rapidi, suscitando al tempo stesso non poche congetture. Da scenari distopici in stile 2001: Odissea nello spazio, passando…
Il settore tech è appannaggio esclusivo della popolazione maschile? Questo preconcetto è abbastanza diffuso, ma come stanno davvero le cose? Oggigiorno il mondo tech, non solo in Italia, è in gran …

Seguici su Facebook

Scopri di più sul corso Hackademy

Inizia la tua nuova carriera nel mondo digital e tech.

Candidati ora

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

Pagamento rateale

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

Esempio di finanziamento  

Importo finanziato: € 2440 in 24 rate da € 112 – TAN fisso 9,37% TAEG 12,57% – importo totale del credito € 2788,68.

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/07/2023 al 31/12/2023.

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 183 €/mese.

Esempio di finanziamento 

Importo finanziato: € 3990 in 24 rate da € 183 – TAN fisso 9,37% TAEG 12,57% – importo totale del credito € 4496,56.

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/07/2023 al 31/12/2023.

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.