Le idee di programmazione strutturata di Edsger Dijkstra spiegano perché codice disciplinato e semplice resta corretto e manutenibile quando crescono team, funzionalità e sistemi.

Il software raramente fallisce perché non può essere scritto. Fallisce perché, un anno dopo, nessuno può modificarlo in sicurezza.
Man mano che i codebase crescono, ogni piccola modifica inizia a propagarsi: una correzione di bug rompe una funzionalità lontana, un nuovo requisito costringe a riscritture e un semplice refactor si trasforma in una settimana di coordinamento accurato. La parte difficile non è aggiungere codice—è mantenere il comportamento prevedibile mentre tutto intorno cambia.
Edsger Dijkstra sosteneva che correttezza e semplicità dovrebbero essere obiettivi di primo piano, non optional. Il ritorno non è accademico. Quando un sistema è più facile da ragionare, i team passano meno tempo a spegnere incendi e più tempo a costruire.
Quando si parla di “scalare”, spesso si intende le prestazioni. Il punto di Dijkstra è diverso: anche la complessità scala.
La scala si manifesta come:
La programmazione strutturata non è severità fine a se stessa. È scegliere flussi di controllo e decomposizioni che rendono facili le risposte a due domande:
Quando il comportamento è prevedibile, il cambiamento diventa di routine invece che rischioso. Per questo Dijkstra è ancora importante: la sua disciplina colpisce il vero collo di bottiglia del software che cresce—capirlo abbastanza bene da migliorarlo.
Edsger W. Dijkstra (1930–2002) è stato un informatico olandese che ha influenzato il modo in cui i programmatori pensano alla costruzione di software affidabile. Ha lavorato su sistemi operativi precoci, ha contribuito agli algoritmi (compreso l'algoritmo del percorso più breve che porta il suo nome) e—soprattutto per gli sviluppatori quotidiani—ha promosso l'idea che programmare debba essere qualcosa su cui possiamo ragionare, non solo qualcosa che proviamo finché sembra funzionare.
A Dijkstra interessava meno che un programma potesse produrre l'output giusto per pochi esempi e più il fatto che potessimo spiegare perché è corretto per i casi che contano.
Se puoi dichiarare cosa dovrebbe fare un pezzo di codice, dovresti essere in grado di argomentare (passo dopo passo) che lo fa davvero. Questo atteggiamento porta naturalmente a codice più facile da seguire, più facile da revisionare e meno dipendente da debug eroici.
Alcuni scritti di Dijkstra suonano inflessibili. Criticava i trucchi “intelligenti”, i flussi di controllo approssimativi e le abitudini di codifica che rendono il ragionamento difficile. La severità non è un gusto stilistico; è ridurre l'ambiguità. Quando il significato del codice è chiaro, si spende meno tempo a discutere le intenzioni e più tempo a validare il comportamento.
La programmazione strutturata è la pratica di costruire programmi a partire da un piccolo insieme di chiare strutture di controllo—sequenza, selezione (if/else) e iterazione (loop)—invece di salti aggrovigliati nel flusso. L'obiettivo è semplice: rendere il percorso attraverso il programma comprensibile così da poterlo spiegare, mantenere e modificare con fiducia.
Le persone spesso descrivono la qualità del software come “veloce”, “bella” o “ricca di funzionalità”. Gli utenti vivono la correttezza in modo diverso: come la fiducia silenziosa che l'app non li sorprenderà. Quando la correttezza è presente, nessuno se ne accorge. Quando manca, tutto il resto smette di avere importanza.
“Funziona ora” solitamente significa che hai provato qualche percorso e ottenuto il risultato atteso. “Continua a funzionare” significa che si comporta come previsto nel tempo, nei casi limite e con i cambiamenti—dopo refactor, nuove integrazioni, traffico più alto e nuovi membri del team che toccano il codice.
Una funzionalità può “funzionare ora” pur restando fragile:
La correttezza riguarda la rimozione di queste assunzioni nascoste—o il renderle esplicite.
Un bug minore raramente resta minore quando il software cresce. Uno stato scorretto, un off-by-one o una regola di gestione degli errori poco chiara vengono copiati in nuovi moduli, avvolti da altri servizi, memorizzati in cache, ritentati o “aggirati”. Col tempo, i team smettono di chiedersi “cos'è vero?” e iniziano a chiedersi “cosa succede di solito?” È allora che la risposta agli incidenti diventa archeologia.
Il moltiplicatore è la dipendenza: un piccolo malfunzionamento diventa molti malfunzionamenti a valle, ognuno con la propria soluzione parziale.
Il codice chiaro migliora la correttezza perché migliora la comunicazione:
La correttezza significa: per gli input e le situazioni che dichiariamo di supportare, il sistema produce coerentemente gli esiti promessi—fallendo in modi prevedibili e spiegabili quando non può farlo.
La semplicità non riguarda rendere il codice “carino”, minimale o intelligente. Riguarda rendere il comportamento facile da prevedere, spiegare e modificare senza paura. Dijkstra valorizzava la semplicità perché migliora la nostra capacità di ragionare sui programmi—soprattutto quando il codebase e il team crescono.
Il codice semplice mantiene in movimento un piccolo numero di idee: flusso dati chiaro, flusso di controllo chiaro e responsabilità ben definite. Non costringe il lettore a simulare molte strade alternative nella testa.
La semplicità non è:
Molti sistemi diventano difficili da cambiare non perché il dominio sia intrinsecamente complesso, ma perché introduciamo complessità accidentale: flag che interagiscono in modi imprevisti, patch per casi speciali che non vengono rimosse e livelli che esistono per aggirare decisioni precedenti.
Ogni eccezione in più è una tassa sulla comprensione. Il costo si manifesta più tardi, quando qualcuno tenta di correggere un bug e scopre che una modifica in un'area rompe sottilmente altre.
Quando un design è semplice, il progresso viene da lavoro costante: modifiche recensibili, diff piccoli e meno fix d'emergenza. I team non hanno bisogno di sviluppatori “eroi” che ricordano ogni caso limite storico o che sanno fare debug alle 2 di notte. Invece, il sistema supporta normale attenzione umana.
Un test pratico: se continui ad aggiungere eccezioni (“a meno che…”, “eccetto quando…”, “solo per questo cliente…”), probabilmente stai accumulando complessità accidentale. Preferisci soluzioni che riducono il branching nel comportamento—una regola coerente batte cinque casi speciali, anche se la regola coerente è un po' più generale di quanto immaginavi inizialmente.
La programmazione strutturata è un'idea semplice con grandi conseguenze: scrivi codice in modo che il suo percorso di esecuzione sia facile da seguire. In parole semplici, la maggior parte dei programmi può essere costruita da tre blocchi costitutivi—sequenza, selezione e ripetizione—senza affidarsi a salti aggrovigliati.
if/else, switch).for, while).Quando il flusso di controllo è composto da queste strutture, di solito puoi spiegare cosa fa il programma leggendo dall'alto in basso, senza “teletrasportarti” nel file.
Prima che la programmazione strutturata diventasse la norma, molti codebase si affidavano pesantemente a salti arbitrari (il classico flusso goto). Il problema non è che i salti siano sempre cattivi; è che il salto non regolamentato crea percorsi di esecuzione difficili da prevedere. Finisci per farti domande come “Come ci siamo arrivati qui?” e “In quale stato è questa variabile?”—e il codice non risponde chiaramente.
Un flusso di controllo chiaro aiuta le persone a costruire un modello mentale corretto. Quel modello è ciò su cui fai affidamento quando fai debug, rivedi una pull request o cambi comportamento sotto pressione.
Quando la struttura è consistente, la modifica diventa più sicura: puoi cambiare un ramo senza influenzarne un altro, o rifattorizzare un loop senza perdere un percorso di uscita nascosto. La leggibilità non è solo estetica—è la base per cambiare comportamento con fiducia senza rompere ciò che già funziona.
Dijkstra propose un'idea semplice: se puoi spiegare perché il tuo codice è corretto, puoi cambiarlo con meno paura. Tre piccoli strumenti di ragionamento rendono questo pratico—senza trasformare il team in matematici.
Un'invariante è un fatto che resta vero mentre una porzione di codice gira, specialmente dentro un loop.
Esempio: stai sommando i prezzi nel carrello. Un'invariante utile è: “total è la somma di tutti gli articoli processati finora.” Se questo resta vero ad ogni passo, quando il loop finisce il risultato è affidabile.
Le invarianti sono potenti perché focalizzano l'attenzione su ciò che non deve mai rompersi, non solo su ciò che dovrebbe succedere dopo.
Una precondizione è ciò che deve essere vero prima che una funzione venga eseguita. Una postcondizione è ciò che la funzione garantisce dopo che termina.
Esempi quotidiani:
Nel codice, una precondizione potrebbe essere “la lista in input è ordinata”, e la postcondizione potrebbe essere “la lista in output è ordinata e contiene gli stessi elementi più quello inserito”.
Quando le annoti (anche informalmente), il design diventa più nitido: decidi cosa una funzione si aspetta e cosa promette, e naturalmente la rendi più piccola e focalizzata.
Nelle review, sposta il dibattito dallo stile (“Lo scriverei diversamente”) verso la correttezza (“Questa funzione mantiene l'invariante?” “Enforziamo la precondizione o la documentiamo?”).
Non servono prove formali per beneficiare. Scegli il loop più bug-prone o l'aggiornamento di stato più ostico e aggiungi un'invariante di una riga sopra di esso. Quando qualcuno modifica il codice dopo, quel commento funge da barriera: se una modifica rompe questo fatto, il codice non è più sicuro.
Test e ragionamento mirano allo stesso risultato—software che si comporta come previsto—ma funzionano in modi molto diversi. I test scoprono problemi provando esempi. Il ragionamento previene intere categorie di problemi rendendo la logica esplicita e verificabile.
I test sono una rete di sicurezza pratica. Catturano regressioni, verificano scenari reali e documentano il comportamento atteso in modo che tutto il team possa eseguirli.
Ma i test possono solo mostrare la presenza di bug, non la loro assenza. Nessuna suite di test copre ogni input, ogni variazione temporale o ogni interazione tra funzionalità. Molti fallimenti “funziona sulla mia macchina” derivano da combinazioni non testate: un input raro, un ordine specifico di operazioni o uno stato sottile che appare dopo diversi passi.
Il ragionamento riguarda la prova di proprietà del codice: “questo loop termina sempre”, “questa variabile non è mai negativa”, “questa funzione non restituisce un oggetto invalido”. Ben fatto, esclude intere classi di difetti—specialmente sui bordi e nei casi limite.
Il limite è lo sforzo e la portata. Prove formali complete per un intero prodotto sono raramente economiche. Il ragionamento funziona meglio se applicato selettivamente: algoritmi core, flussi sensibili alla sicurezza, logica di pagamento e concorrenza.
Usa i test in modo ampio e applica ragionamento più profondo dove il fallimento è costoso.
Un ponte pratico tra i due è rendere l'intento eseguibile:
Queste tecniche non sostituiscono i test—they stringono la rete. Trasformano aspettative vaghe in regole verificabili, rendendo i bug più difficili da introdurre e più facili da diagnosticare.
Il codice “intelligente” spesso sembra una vittoria nel momento: meno righe, un trucco elegante, un one-liner che fa sentire brillanti. Il problema è che quella furbizia non scala nel tempo né tra le persone. Sei mesi dopo, l'autore dimentica il trucco. Un nuovo collega lo legge alla lettera, perde l'assunzione nascosta e lo cambia rompendo il comportamento. Questo è il “debito di furbizia”: velocità a breve termine comprata con confusione a lungo termine.
Il punto di Dijkstra non era “scrivi codice noioso” come preferenza stilistica—era che vincoli disciplinati rendono i programmi più facili da ragionare. In un team, i vincoli riducono anche l'affaticamento decisionale. Se tutti conoscono già i default (naming, struttura delle funzioni, cosa significa “done”), smetti di ridiscutere le basi in ogni pull request. Quel tempo ritorna al lavoro di prodotto.
La disciplina si manifesta in pratiche di routine:
Alcune abitudini concrete che prevengono l'accumulo di debito di furbizia:
calculate_total() a do_it()).La disciplina non riguarda la perfezione—riguarda rendere prevedibile la prossima modifica.
La modularità non è solo “dividere il codice in file.” È isolare le decisioni dietro confini chiari, così il resto del sistema non deve sapere (o preoccuparsi) dei dettagli interni. Un modulo nasconde le parti disordinate—strutture dati, casi limite, ottimizzazioni di performance—mentre espone una superficie piccola e stabile.
Quando arriva una richiesta di modifica, l'esito ideale è: un solo modulo cambia e tutto il resto resta intatto. Questo è il significato pratico di “mantenere la modifica locale.” I confini prevengono l'accoppiamento accidentale—dove aggiornare una funzionalità rompe silenziosamente tre altre perché condividevano assunzioni.
Un buon confine rende anche il ragionamento più semplice. Se puoi dichiarare cosa garantisce un modulo, puoi ragionare sul programma più ampio senza rileggere tutta l'implementazione ogni volta.
Un'interfaccia è una promessa: “Dato questo input, produrrò questo output e manterrò queste regole.” Quando la promessa è chiara, i team possono lavorare in parallelo:
Non è burocrazia—è creare punti di coordinamento sicuri in un codebase che cresce.
Non serve una grande revisione architetturale per migliorare la modularità. Prova questi controlli leggeri:
Confini ben disegnati trasformano la “modifica” da evento di sistema a edit localizzato.
Quando il software è piccolo, puoi “tenerlo tutto in testa.” Alla scala, questo smette di essere vero—e i modi in cui fallisce diventano familiari.
I sintomi comuni sono:
La scommessa centrale di Dijkstra era che gli umani sono il collo di bottiglia. Flusso di controllo chiaro, unità piccole e ben definite e codice che puoi ragionare non sono scelte estetiche—sono moltiplicatori di capacità.
In un grande codebase, la struttura agisce come compressione della comprensione. Se le funzioni hanno input/output espliciti, i moduli hanno confini nominabili e il “percorso felice” non è aggrovigliato con ogni caso limite, gli sviluppatori passano meno tempo a ricostruire l'intento e più tempo a fare cambiamenti deliberati.
Man mano che i team crescono, i costi di comunicazione aumentano più velocemente delle linee di codice. Il codice disciplinato e leggibile riduce la quantità di conoscenza tribale necessaria per contribuire in sicurezza.
Questo si vede subito nell'onboarding: i nuovi ingegneri possono seguire pattern prevedibili, imparare un piccolo insieme di convenzioni e fare modifiche senza un lungo tour dei “gotcha”. Il codice stesso insegna il sistema.
Durante un incidente il tempo è scarso e la fiducia è fragile. Il codice scritto con assunzioni esplicite (precondizioni), controlli significativi e flusso di controllo lineare è più facile da tracciare sotto pressione.
Ancora più importante, le modifiche disciplinate sono più facili da rollbackare. Edit più piccoli e localizzati con confini chiari riducono la probabilità che un rollback inneschi nuovi guasti. Il risultato non è perfezione—sono meno sorprese, recupero più rapido e un sistema che resta manutenibile con gli anni e i contributori.
Il punto di Dijkstra non era “scrivi codice all'antica.” Era “scrivi codice che puoi spiegare.” Puoi adottare questo mindset senza trasformare ogni feature in un esercizio di prova formale.
Inizia con scelte che rendono economico il ragionamento:
Una buona euristica: se non riesci a riassumere in una frase cosa garantisce una funzione, probabilmente fa troppo.
Non serve un grande sprint di refactor. Aggiungi struttura alle cuciture:
isEligibleForRefund).Questi upgrade sono incrementali: riducono il carico cognitivo per la modifica successiva.
Quando revisioni (o scrivi) una modifica, chiedi:
Se i reviewer non possono rispondere velocemente, il codice sta segnalando dipendenze nascoste.
I commenti che ripetono il codice invecchiano. Scrivi perché il codice è corretto: le assunzioni chiave, i casi limite che proteggi e cosa si romperebbe se quelle assunzioni cambiassero. Una nota breve come “Invariante: total è sempre la somma degli elementi processati” può valere più di un paragrafo di narrazione.
Se vuoi un posto leggero per raccogliere queste abitudini, mettile in una checklist condivisa (vedi /blog/practical-checklist-for-disciplined-code).
I team moderni usano sempre più l'AI per accelerare la delivery. Il rischio è noto: velocità oggi può trasformarsi in confusione domani se il codice generato è difficile da spiegare.
Un modo conforme a Dijkstra di usare l'AI è trattarla come un acceleratore del pensiero strutturato, non come un suo sostituto. Per esempio, quando costruisci in Koder.ai—una piattaforma vibe-coding dove crei app web, backend e mobile tramite chat—puoi mantenere le abitudini “prima il ragionamento” rendendo espliciti prompt e passi di review:
Anche se poi esporti il codice sorgente ed esegui altrove, lo stesso principio vale: il codice generato deve essere codice che sai spiegare.
Questa è una checklist “amica di Dijkstra” leggera che puoi usare durante review, refactor o prima di mergiare. Non si tratta di fare prove tutto il giorno—si tratta di rendere correttezza e chiarezza l'impostazione predefinita.
total è la somma degli articoli processati” previene bug sottili.Scegli un modulo disordinato e rifattorizza prima il flusso di controllo:
Poi aggiungi alcuni test mirati attorno ai nuovi confini. Se vuoi altri pattern come questo, guarda i post correlati su /blog.
Perché, quando i codebase crescono, il collo di bottiglia principale diventa capire il sistema — non scrivere il codice. L'enfasi di Dijkstra sul controllo prevedibile del flusso, contratti chiari e correttezza riduce il rischio che una “piccola modifica” provochi comportamenti sorprendenti altrove, che è proprio ciò che rallenta i team nel tempo.
In questo contesto, “scalare” riguarda meno le prestazioni e più la moltiplicazione della complessità:
Queste forze rendono il ragionamento e la prevedibilità più preziosi della “furbizia” del codice.
La programmazione strutturata favorisce un piccolo insieme di chiare strutture di controllo:
if/else, switch)for, while)L'obiettivo non è la rigidità, ma rendere i percorsi di esecuzione facili da seguire, così puoi spiegare il comportamento, rivedere le modifiche e fare debug senza “teletrasportarti” nel codice.
Il problema è il salto non regolamentato che crea percorsi difficili da prevedere e stato poco chiaro. Quando il flusso di controllo è aggrovigliato, gli sviluppatori sprecano tempo a rispondere a domande basilari come “Come ci siamo arrivati qui?” e “Quale stato è valido adesso?”.
Equivalenti moderni includono ramificazioni profondamente annidate, uscite anticipate sparse e modifiche implicite di stato che rendono il comportamento difficile da tracciare.
La correttezza è la “feature silenziosa” di cui gli utenti si fidano: il sistema fa costantemente ciò che promette e fallisce in modi prevedibili e spiegabili quando non può fare altrimenti. È la differenza tra “funziona in alcuni esempi” e “continua a funzionare dopo refactor, integrazioni e casi limite”.
Perché le dipendenze amplificano gli errori. Un piccolo stato errato o un bug di bordo viene copiato, memorizzato nella cache, ritentato, avvolto e “aggirato” tra moduli e servizi. Col tempo i team smettono di chiedersi “cos'è vero?” e iniziano a contare su “cosa succede di solito”, il che rende gli incidenti più difficili e le modifiche più rischiose.
La semplicità significa poche idee in movimento simultaneamente: responsabilità chiare, flusso dati chiaro e casi speciali minimizzati. Non si tratta di scrivere meno righe o one-liner intelligenti.
Una buona prova è se il comportamento rimane prevedibile quando i requisiti cambiano. Se ogni nuovo caso aggiunge regole “a meno che…”, si sta accumulando complessità accidentale.
Un'invariante è un fatto che deve rimanere vero durante un loop o una transizione di stato. Un modo leggero per usarla:
total è la somma degli elementi processati”)Questo rende le modifiche successive più sicure perché il prossimo sviluppatore sa cosa non deve rompersi.
I test individuano bug provando esempi; il ragionamento previene intere classi di bug rendendo la logica esplicita. I test non possono provare l'assenza di difetti perché non coprono ogni input o variazione temporale. Il ragionamento è particolarmente utile per aree ad alto costo di errore (denaro, sicurezza, concorrenza).
Un mix pratico è: test ampi + assertion mirate + precondizioni/postcondizioni chiare attorno alla logica critica.
Inizia con mosse piccole e ripetibili che riducono il carico cognitivo:
Questi sono upgrade incrementali di struttura che rendono più economico il cambiamento successivo senza richiedere un rewrite.