Il codice che non scrivi

Less is more: astrazioni premature, dipendenze inutili e il costo della complessità.

31 ottobre 2026 · Circa 10 minuti di lettura

Con gli agenti AI il lavoro dello sviluppatore è cambiato. Basta chiedere di implementare una feature e il codice che fino a poco tempo fa avrebbe richiesto un intero pomeriggio di lavoro viene prodotto in pochi minuti, completo di test automatici e, a volte, persino con la soluzione a problemi che non avevamo individuato.

La potenza di questi strumenti è innegabile, ma è facile farsi prendere dall'entusiasmo e dimenticare che il codice prodotto ha un costo che va ben oltre il tempo necessario a scriverlo. Cosa succederà quando qualcuno, magari noi stessi, dovrà rimetterci mano tra sei mesi o due anni, senza avere più alcuna traccia della conversazione originale con l'agente?

Il costo di scrivere codice è crollato. Quello di mantenerlo, molto meno.

Il conto arriva dopo il merge

Quando sviluppiamo una nuova funzionalità, la prima cosa che ci chiediamo è quanto tempo ci vorrà, tenendo conto anche del tempo necessario per scrivere i test e verificare che tutto funzioni correttamente. Superati i test e la validazione, tiriamo un sospiro di sollievo perché riteniamo il lavoro concluso quando, in realtà, il codice che abbiamo scritto ha appena iniziato la sua vita.

Ogni volta che scriviamo del codice introduciamo potenziali bug e, anche quando funziona perfettamente, i comportamenti che abbiamo aggiunto andranno tenuti in considerazione quando implementeremo nuove funzionalità. Lo stesso vale per le dipendenze e le scelte architetturali: tutto ciò che aggiungiamo a un progetto deve essere compreso, aggiornato e mantenuto.

A essere determinante non è tanto la quantità di codice che scriviamo, quanto la complessità che stiamo promettendo di gestire nel tempo. I benefici di una modifica sono immediatamente visibili, mentre i costi emergono spesso mesi, se non anni, dopo. È anche così che si accumula il debito tecnico: una serie di decisioni individualmente ragionevoli, almeno all'apparenza, che complessivamente rendono il sistema più difficile da mantenere.

Introdurre complessità in un progetto è spesso inevitabile e non ogni modifica rappresenta debito tecnico, ma il beneficio dovrebbe sempre giustificare il costo futuro che comporta.

L'astrazione che aspetta il suo secondo caso d'uso

Uno dei modi più comuni di introdurre complessità non necessaria è quello di creare astrazioni prima di averne realmente bisogno. Immaginiamo di dover implementare un servizio con una sola implementazione: potremmo limitarci a scrivere il codice necessario al suo funzionamento, oppure definire un'interfaccia, una factory e un meccanismo che permetta di scegliere tra diverse implementazioni.

La seconda soluzione potrebbe sembrare più curata dal punto di vista architetturale, perché ci permetterebbe di sostituire l'implementazione senza modificare il codice che la utilizza. Ma se non abbiamo alcun motivo concreto per pensare che avremo bisogno di farlo, stiamo aggiungendo complessità per risolvere un problema che non esiste.

Se in futuro dovesse presentarsi la necessità di introdurre una seconda implementazione, potremo creare l'astrazione in quel momento, quando avremo due casi d'uso reali sui quali basarci. Probabilmente saremo anche in grado di progettarla meglio, perché sapremo quali comportamenti devono essere condivisi e quali devono rimanere indipendenti. Anticipare questa decisione significa invece basarci su supposizioni che potrebbero rivelarsi sbagliate.

Prevedere esigenze future è importante, soprattutto quando modificare successivamente l'architettura comporterebbe costi elevati. Ma non tutte le decisioni devono essere prese subito: rimandarne alcune ci permette di avere informazioni migliori quando arriverà il momento di affrontarle. Prima di introdurre un'astrazione dovremmo quindi chiederci se risponde a un'esigenza concreta oppure soltanto alla possibilità che un giorno possa tornarci utile.

Qualche riga duplicata può andare bene

Uno dei principi più conosciuti nello sviluppo software è DRY (Don't Repeat Yourself), secondo il quale dovremmo evitare di duplicare la stessa logica in più punti del codice. È un principio importante, perché quando una regola viene implementata più volte rischiamo di modificarla in un punto e dimenticarci di aggiornarla negli altri, introducendo comportamenti incoerenti.

Il problema nasce quando confondiamo la duplicazione della logica con la semplice somiglianza tra due porzioni di codice. Immaginiamo due funzioni che eseguono operazioni simili, ma che appartengono a parti diverse del sistema e rispondono a esigenze indipendenti. Potremmo estrarre il comportamento comune in una funzione condivisa, eliminando la duplicazione e riducendo il numero di righe. In apparenza abbiamo semplificato il codice, ma abbiamo anche creato una dipendenza tra due componenti che prima potevano evolvere separatamente.

Quando una delle due funzioni dovrà cambiare comportamento, potremmo trovarci costretti ad aggiungere parametri o condizioni alla funzione condivisa per gestire le differenze. Con il passare del tempo, quella che inizialmente sembrava una buona astrazione potrebbe diventare più complicata delle due implementazioni originali.

Duplicare regole che devono rimanere coerenti è certamente rischioso, ma qualche riga ripetuta può essere preferibile a un'astrazione che introduce dipendenze non necessarie. Prima di eliminare una duplicazione dovremmo chiederci se i due blocchi rappresentano davvero lo stesso comportamento, destinato a evolvere insieme, oppure se si assomigliano soltanto per coincidenza.

Il codice morto e quello che dorme

Un'altra fonte di complessità spesso sottovalutata è il codice che esiste all'interno di un progetto ma che non viene utilizzato. Il caso più semplice è quello del codice morto: vecchie funzioni che non vengono più richiamate, implementazioni superate o percorsi che non possono più essere eseguiti. Una volta verificato che non servono, possiamo eliminarli senza troppi rimpianti, anche perché i sistemi di versionamento ci permettono di recuperarli qualora dovessimo averne nuovamente bisogno.

Più interessante è invece il caso di quello che potremmo definire codice dormiente: funzionalità perfettamente funzionanti, magari anche coperte da test, che però non rispondono ad alcuna esigenza attuale. Potrebbe trattarsi di un sistema di plugin che nessuno ha mai utilizzato, di un'opzione di configurazione prevista per una modalità mai utilizzata o di un meccanismo di estensibilità introdotto per supportare eventuali sviluppi futuri.

A differenza del codice morto, quello dormiente potrebbe effettivamente tornare utile, ed è proprio questo a renderne difficile l'eliminazione. Nel frattempo, però, continua a richiedere manutenzione: quando aggiorniamo una dipendenza dobbiamo verificarne la compatibilità, quando modifichiamo l'architettura dobbiamo tenerne conto e chiunque lavori al progetto deve comprenderne il funzionamento.

Il discorso è diverso per quei componenti che devono essere disponibili anche se utilizzati raramente, come un sistema di ripristino in caso di emergenza: in quel caso abbiamo un'esigenza concreta, per quanto eccezionale. Il codice dormiente di cui dovremmo preoccuparci è quello che abbiamo implementato soltanto perché un giorno potrebbe servirci. Intanto però il conto della manutenzione arriva anche se quel giorno non arriva mai.

La soluzione noiosa

Quando dobbiamo risolvere un problema siamo spesso tentati di scegliere la soluzione tecnicamente più sofisticata, anche quando ne esiste una più semplice che soddisferebbe perfettamente le nostre esigenze. Immaginiamo di dover gestire alcune impostazioni di un'applicazione: potremmo utilizzare un file di configurazione oppure sviluppare un pannello di controllo completo di interfaccia web, API, sistema di persistenza e gestione delle autorizzazioni.

La seconda soluzione è sicuramente più ricca di funzionalità, ma se quelle impostazioni vengono modificate un paio di volte all'anno da un amministratore che ha già accesso al repository, quale vantaggio concreto stiamo ottenendo? Abbiamo aggiunto diversi componenti che dovremo mantenere, aggiornare e proteggere, quando un file di configurazione avrebbe risolto lo stesso problema con una frazione della complessità.

Un ragionamento simile si può applicare alle prestazioni. Ottimizzare un algoritmo, introdurre un sistema di caching o parallelizzare un'operazione possono portare miglioramenti significativi, ma senza misurazioni che dimostrino l'esistenza di un problema rischiamo di complicare il codice senza ottenere alcun beneficio percepibile. Una cache, ad esempio, introduce problematiche legate all'invalidazione dei dati e alla loro consistenza che potrebbero essere ingiustificate rispetto al vantaggio ottenuto.

Scegliere la semplicità non significa necessariamente scrivere meno righe di codice, né rinunciare alle prestazioni quando sono necessarie. Significa preferire soluzioni leggibili, prevedibili e facili da mantenere, evitando la complessità che non siamo in grado di giustificare con requisiti concreti. Una soluzione meno sofisticata può sembrare poco interessante dal punto di vista tecnico, ma se risolve il problema senza aggiungere ulteriori responsabilità al progetto, probabilmente è quella che dovremmo scegliere.

Quando l'agente vuole essere troppo bravo

La tendenza a complicare inutilmente il codice non è certo nata con l'intelligenza artificiale. Gli sviluppatori hanno sempre avuto la tentazione di creare soluzioni più elaborate del necessario, introducendo astrazioni, framework e meccanismi di estensibilità per esigenze future. Chi ha qualche anno di esperienza ha probabilmente mantenuto almeno una di quelle architetture pensate per un futuro che non si è mai presentato.

La differenza è che oggi, grazie agli agenti AI, produrre questo genere di codice è diventato estremamente facile. Chiediamo di gestire un errore e ci ritroviamo con un sistema generalizzato di error handling, oppure chiediamo di aggiungere un parametro e l'agente decide di introdurre un'infrastruttura di configurazione. Sono interventi che possono sembrare miglioramenti, soprattutto perché vengono accompagnati da test, documentazione e spiegazioni che ne giustificano l'utilità. Il risultato è un lavoro apparentemente più completo di quello che avevamo richiesto.

Prima dell'arrivo di questi strumenti, realizzare una soluzione del genere avrebbe richiesto un investimento di tempo considerevole, che quantomeno ci avrebbe spinto a valutarne l'effettiva necessità. Ora che possiamo ottenere lo stesso risultato in pochi minuti, rischiamo di accettare modifiche non necessarie semplicemente perché sono già state implementate e sembrano funzionare correttamente.

Il tempo risparmiato nella scrittura, però, non elimina la necessità di comprendere e mantenere tutto ciò che è stato generato. Valutare la produttività di un agente in base alla quantità di codice prodotto o al numero di miglioramenti introdotti può quindi essere fuorviante. Una modifica di poche righe che risolve esattamente il problema richiesto potrebbe essere preferibile a una soluzione molto più articolata, anche quando quest'ultima è tecnicamente corretta.

Quando un agente introduce un bug, possiamo individuarlo, riprodurlo e correggerlo. Ma se produce una soluzione tecnicamente corretta, ben strutturata e perfettamente funzionante, potremmo non avere alcun motivo apparente per metterla in discussione, anche quando introduce complessità di cui non avevamo bisogno. È il buon codice che risolve un problema che non abbiamo.

Il codice che non scrivi

Siamo abituati a valutare il lavoro di uno sviluppatore attraverso ciò che produce: funzionalità implementate, problemi risolti, pull request integrate. Sono risultati concreti, che possiamo mostrare e in qualche modo misurare. È molto più difficile riconoscere il valore di qualcosa che abbiamo deliberatamente scelto di non realizzare.

Non sapremo mai quanti bug abbiamo evitato rinunciando a una funzionalità superflua, quante dipendenze non dovremo aggiornare o quanto tempo risparmierà chi, tra qualche anno, non sarà costretto a comprendere un'astrazione di cui non avevamo realmente bisogno. Sono benefici difficili da quantificare, ma che nel corso della vita di un progetto possono fare una differenza considerevole.

Con gli strumenti di intelligenza artificiale saremo in grado di produrre sempre più codice in sempre meno tempo, ma non possiamo dare per scontato che la nostra capacità di comprenderlo, verificarlo e mantenerlo cresca allo stesso ritmo. Credo quindi che il ruolo dello sviluppatore sarà sempre più legato alla capacità di valutare ciò che viene prodotto, riconoscendo quando una soluzione è inutilmente complicata, quando un'astrazione è prematura o quando una funzionalità, per quanto ben realizzata, non aggiunge alcun valore concreto al progetto.

Dovremo imparare a essere più selettivi, anche quando aggiungere qualcosa richiede soltanto pochi minuti. A volte questo significherà rinunciare a un'ottimizzazione, accettare qualche riga di codice duplicata oppure rimandare una decisione fino a quando avremo informazioni sufficienti per prenderla. Altre volte significherà semplicemente eliminare una parte del codice che un agente ha appena scritto.

Scrivere codice sta diventando sempre più facile, ma decidere quale codice meriti di esistere sempre più difficile.

E il codice che scegliamo di non scrivere non dovrà mai essere corretto, aggiornato o spiegato a chi verrà dopo di noi.