Sei abitudini di un senior developer che valgono più di dieci anni di esperienza: cosa difendo davvero in code review nelle PMI

Sei abitudini di un senior developer che valgono più di dieci anni di esperienza: cosa difendo davvero in code review nelle PMI

Nel febbraio 2025 sono stato chiamato come consulente per un progetto Laravel di una società di logistica veneta che gestiva la tracciabilità di circa 12.000 spedizioni al giorno con tre sviluppatori fissi. Il loro tech lead aveva passato due settimane a "ottimizzare" la query principale del modulo di tracking: l'aveva portata da 180ms a 35ms su benchmark in locale, aggiungendo un index hint forzato e riscrivendo una parte della join. Il giorno del deploy, la stessa query in produzione su pattern di lookup che il dataset di test non riproduceva ha portato MySQL al 100% di CPU per quasi un'ora. Il commit message era una riga sola: "ottimizzata query tracking". Niente prima/dopo, niente piano di rollback, niente discussione preventiva. Crisi alle 14:00, rollback alle 15:20, mezza giornata di order intake bruciata. Quando ho commentato che "il problema non è la query, è il processo", la risposta del tech lead è stata: "ma l'ho testata, andava più veloce". È lì che si vede la differenza fra un dev competente sul linguaggio e un senior developer.

Non sto scrivendo un articolo motivazionale sulle "qualità del bravo programmatore" - di quelli ne trovi a quintali su LinkedIn. Sto scrivendo le sei abitudini operative che impongo come standard nelle code review dei team PMI con cui lavoro, e cosa cambia nelle metriche del team (incident frequency, time to restore, percentuale di codice riscritto ogni trimestre) quando un team le adotta tutte. Non sono opinioni: sono regole che ho rifinito su una decina di team negli ultimi cinque anni e che sopravvivono al "ma noi siamo speciali" della prima riunione.

Cosa significa davvero "done" in produzione, e perché il merge non basta mai?

Significa quattro cose insieme: il codice gira in produzione, esiste una metrica nuova nel dashboard di monitoraggio che ti dice se sta funzionando, c'è un piano di rollback documentato e testato in staging, e qualcuno che non sei tu ha verificato il comportamento atteso su dati reali. Tutto il resto è "compilato", non "fatto".

Questa definition of done estesa è esattamente quella che propongono Andrew Hunt e David Thomas in The Pragmatic Programmer (Addison-Wesley, 20th anniversary edition), e che ho visto fallire decine di volte in produzione quando viene tagliata a metà. Il caso veneto dell'incipit è il prototipo: il developer ha considerato il task chiuso al merge, nessuno ha verificato su dataset rappresentativi, nessuno ha scritto un piano di rollback. Il valore che la riscrittura avrebbe dovuto generare era forse 50ms di latency su un endpoint chiamato 12.000 volte al giorno. Net negativo.

La regola operativa che impongo nelle code review è una sola modifica al template di pull request del repository, che obbliga l'autore a rispondere esplicitamente alla domanda di monitoraggio prima di chiedere la review:

<!-- file: .github/pull_request_template.md -->

## Cosa cambia

<!-- Descrizione concisa del problema risolto -->

## Definition of done

- [ ] Il codice è in produzione (non solo in CI)
- [ ] Esiste una metrica nuova nel dashboard che indica se il cambiamento funziona
- [ ] Piano di rollback documentato e testato in staging
- [ ] Verifica eseguita da un membro del team diverso dall'autore

## Come saprò se questo cambiamento sta funzionando bene in produzione?

<!-- Risposta obbligatoria. Se vuota o "lo vedremo se qualcuno si lamenta", la PR non passa. -->

Su un team PMI da tre persone, questa singola regola in sei mesi ha portato la incident frequency da una ogni due settimane a una ogni due mesi. Non perché il codice fosse migliore - il codice era lo stesso - ma perché il processo intercettava i task che non erano realmente finiti.

Leggi codice molto più di quanto ne scrivi: il rapporto 10:1

La seconda abitudine è invisibile a chi misura la produttività in linee di codice prodotte. Un senior passa molto più tempo a leggere codice che a scriverlo: il rapporto realistico nei progetti maturi è 10:1, sui progetti legacy che riprendo in mano sale a 20:1. Leggi prima di toccare qualcosa, leggi dopo per verificare cosa hai rotto, leggi le PR degli altri per capire come ragionano, leggi librerie esterne per capire perché non si comportano come ti aspetti. È un'abitudine così mal vista in alcuni team che ho dovuto difenderla esplicitamente in retrospettiva: "non è che sono lento, è che sto facendo il mio lavoro principale, che non è battere la tastiera".

Il problema è che la lettura del codice non è incentivata da nessuna metrica standard. Nessun KPI ti premia per avere capito davvero come funziona l'autenticazione di un sistema legacy prima di toccarla. Eppure è quello che separa un dev che riprende in mano una codebase PHP legacy senza documentazione e fa modifiche chirurgiche da uno che la riscrive a metà perché "tanto era tutto da rifare". Quando mi chiamano per un audit di un progetto legacy, le prime due settimane le passo a leggere senza scrivere praticamente nulla: annotazioni, diagrammi a mano, query sul DB di staging per capire la forma reale dei dati. I clienti la prima volta si innervosiscono ("ma quando inizia a lavorare?"); dopo aver visto il primo deliverable - mappa delle dipendenze, dei punti di rottura, delle aree sicure su cui intervenire - non lo chiedono più.

Refactoring continuo, non grandi rewrite: la regola del 10%

La terza abitudine è quella che la maggior parte dei senior developer ha imparato dalle proprie cicatrici. Refactoring incrementale, mai grandi rewrite. Quando vedi codice che ti fa schifo, lo migliori del 10% durante la PR che stai già facendo: non apri un branch rewrite-tracking-module che resterà aperto sei mesi e poi sarà mergiato in fretta e furia rompendo metà del sistema. È esattamente il principio del "Boy Scout Rule" del refactoring incrementale, e si lega al concetto di debito tecnico coniato da Ward Cunningham e formalizzato dal Software Engineering Institute della Carnegie Mellon.

Il pattern operativo che applico nei team PMI è semplice: quando lavori su un file, hai il diritto e il dovere di migliorarlo del 10% mentre sei lì, anche se non è strettamente necessario per la feature corrente. Un nome di variabile più chiaro, un'estrazione di metodo, un commento obsoleto rimosso, un dead code path eliminato. Niente che richieda una review separata. In tre mesi, in un team di tre persone con questa regola, la complessità ciclomatica media sulle aree ad alta attività cala del 15-20%, senza un singolo task di "refactoring" mai pianificato a sprint.

L'errore opposto, che vedo sui clienti appena passati per una sostituzione del CTO, è il "great rewrite" che parte con grandi ambizioni e finisce per essere un mostro non testato. Il caso scolastico è quello che Joel Spolsky raccontava nel 2000 in "Things You Should Never Do, Part I" sui rewrite di Netscape, ed è una lezione che invecchia bene. La mia guida pratica al refactoring di codice PHP legacy parte da questa prospettiva: piccoli passi misurabili, mai "ricominciamo da zero".

KISS sopra cleverness: il codice scritto per chi viene dopo di te

La quarta abitudine è la più difficile da insegnare perché va contro l'ego del developer competente. Senior developer scrivono codice boring, prevedibile, facile da leggere. Junior developer scrivono codice clever, condensato, che mostra che hanno padroneggiato il linguaggio. Il codice clever è quello che in PR ti costringe a chiedere spiegazioni; il codice boring è quello che capisci al primo passaggio e che modifichi senza paura il martedì pomeriggio in cui sei sotto deadline. Su sistemi che devono durare cinque anni, è il secondo quello che vuoi.

Un esempio concreto che ho visto bocciare in code review esattamente la settimana scorsa, su un cliente Laravel, ha questa forma:

// Versione "clever" - tutto in un'unica catena Eloquent/Collection
$topTenCustomerIds = collect($orders)
    ->groupBy(fn ($order) => $order->customer_id)
    ->map(fn ($group) => $group->sum(fn ($order) => $order->total - $order->discount))
    ->filter(fn ($net) => $net > 1000)
    ->sortDesc()
    ->take(10)
    ->keys()
    ->toArray();

La versione clever non è "sbagliata": funziona. Il problema è che sei mesi dopo, quando un altro developer dovrà aggiungere "escludi gli ordini cancellati", dovrà mentalmente decomporre la catena, capire dove inserire il filtro, e probabilmente sbagliare il punto di inserimento. La versione boring è più lunga ma immediatamente modificabile:

// Versione "boring" - esplicita, riscrivibile senza paura a qualsiasi passo
$revenuePerCustomer = [];
foreach ($orders as $order) {
    // qui domani puoi aggiungere: if ($order->is_cancelled) { continue; }
    $netTotal = $order->total - $order->discount;
    $customerId = $order->customer_id;
    $revenuePerCustomer[$customerId] = ($revenuePerCustomer[$customerId] ?? 0) + $netTotal;
}

$highValueCustomers = array_filter($revenuePerCustomer, fn ($revenue) => $revenue > 1000);
arsort($highValueCustomers);
$topTenCustomerIds = array_slice(array_keys($highValueCustomers), 0, 10);

Il principio che cito sempre in code review è quello formalizzato da Edsger W. Dijkstra in "The Humble Programmer", Turing Award Lecture, ACM 1972: ridurre la complessità, non esibirla. La regola operativa è una sola: se durante la review devo chiedere "cosa fa questa riga?", la riga va riscritta. Non importa quanto sia elegante, né che usi quella feature nuova del linguaggio. Se la sintesi richiede una spiegazione, la sintesi è sbagliata. Applicato consistentemente, questo principio è quello che porta a sostituire i Fat Controller con Service Layer espliciti nei refactoring Laravel che faccio sui clienti: l'esplicitezza vince sempre sull'eleganza.

C'è un corollario: la cleverness funzionale (algoritmi che riducono la complessità asintotica) è diversa dalla cleverness sintattica (one-liner che condensano dieci operazioni in una riga). La prima è valore, la seconda è ego. Un senior sa distinguere le due e accetta volentieri di essere "meno furbo" sintatticamente per essere più chiaro semanticamente.

Documentation as code: ADR e PR description sono codice di prima classe

La quinta abitudine separa i team che scalano da quelli che si bloccano sopra le sei persone. Un senior sa che la decisione architetturale presa in chat alle 17 di un mercoledì verrà persa in tre mesi, e che chiunque la rimetta in discussione spenderà due giorni a riscoprire perché era stata presa. La soluzione si chiama Architecture Decision Records (ADR), pattern formalizzato da Michael Nygard nel 2011 e oggi mantenuto come canonical reference da Joel Parker Henderson: documenti brevi, mezza pagina al massimo, versionati con il codice nel repository.

Un ADR concreto, di quelli che faccio scrivere ai team che seguo, ha questa forma minimale e vive in docs/adr/0007-passport-migration.md:

<!-- file: docs/adr/0007-passport-migration.md -->

## ADR-007: Migrazione autenticazione API da Sanctum a Passport

### Status

Accepted - 2026-03-12

### Context

Il modulo API esposto al frontend SPA del cliente Acme richiede token
con scadenza configurabile per cliente e refresh token. Sanctum non
supporta nativamente i refresh token; Passport sì.

### Decision

Migriamo l'autenticazione API da Sanctum a Passport. Sanctum rimane
in uso per le sessioni del backoffice interno.

### Consequences

- Aumento di complessità del setup (richiede chiavi RSA, encryption keys).
- Migrazione coordinata dei token attivi (~3.200 utenti) su tre release.
- Permette di chiudere il ticket #427 sull'OAuth2 compliance per Acme.

Mezza pagina. Sei mesi dopo, quando un nuovo developer chiederà "perché non usiamo Sanctum dappertutto?", la risposta è già scritta e versionata. Lo stesso vale per le descrizioni delle pull request: la PR description di un senior non è "ho fatto X come da ticket", è una mini-narrativa che cattura il problema, le opzioni considerate, perché abbiamo scelto questa, e come verificheremo che funzioni. Senza PR description ricche e ADR scritti, ogni modifica diventa un atto di fede e il debito tecnico cresce non perché il codice peggiori ma perché la conoscenza attorno al codice si dissolve. È esattamente il pattern che ho visto generare debito tecnico nei progetti PHP post-subentro su server Linux Hetzner e OVH.

Mai riscrivere la storia di Git in produzione (e cinque altre regole non negoziabili)

La sesta abitudine è un set di regole non negoziabili che ho consolidato negli anni e che sopravvivono al "ma in questo caso speciale...". Sono il livello sotto il quale scendere significa, prima o poi, un incidente di cui pagherà il conto il cliente.

  • Mai git push --force su main o su branch protetti. L'ho visto violato due volte in dieci anni: la prima è costata 11 ore di ricostruzione da backup su uno staging accidentalmente puntato a prod, la seconda 4 ore di PR "perdute" recuperate a mano dal reflog. Se devi annullare qualcosa in storia pubblica, esiste git revert.
  • Mai rinominare colonne di tabelle di produzione in una migrazione single-step. Sempre il pattern in tre release: add-new-column con dual-write nel codice → backfill con uno script idempotente → remove-old-column dopo che il monitoring conferma che nessuno scrive più sulla colonna vecchia.
  • Mai disabilitare i pre-commit hook con --no-verify per "fare un fix veloce". Se l'hook fallisce, capisci perché. Il --no-verify è il sintomo di guardrail di processo non rispettati, e ogni volta che lo concedi autorizzi tacitamente la prossima.
  • Mai introdurre una dipendenza di terze parti senza verificare salute progettuale, manutenzione attiva e licenza. Il framework che applico è derivato dalle quattro categorie del Technology Radar di ThoughtWorks (Adopt, Trial, Assess, Hold): prima di un composer require su un progetto cliente faccio una valutazione esplicita di quale categoria assegnerei a quel pacchetto.
  • Mai loggare informazioni sensibili (token, password, payload PCI, dati personali) nemmeno "solo per debug". Quel Log::debug($request->all()) finirà in un sink di log persistente, e fra sei mesi qualcuno ti chiederà perché in storage/logs/laravel.log ci sono ottantamila numeri di carta di credito in chiaro.
  • Mai accettare un task come "urgente" senza prima verificare cosa cambia nella roadmap del trimestre. La falsa urgenza è il principale generatore di debito tecnico nelle PMI: scelte sub-ottimali fatte per chiudere "in giornata" vivono in produzione per cinque anni.

Queste sei abitudini insieme non sono un manifesto astratto: sono il distillato operativo di dieci anni passati a fare code review su codice altrui, a farmi mordere dai miei errori, e a vedere come la loro combinazione abbatta in modo misurabile incident frequency, time to restore e percentuale di codice riscritto ogni trimestre. Quando un team le adotta tutte, il cambiamento si vede nei tre-quattro mesi successivi: meno hotfix nei weekend, meno discussioni del tipo "ma chi aveva deciso questa cosa?", meno paura di toccare il modulo legacy che tutti evitano da due anni. Se gestisci un team interno e hai la sensazione che il codice prodotto sia "sempre uguale per qualità", o che il debito tecnico stia crescendo silenziosamente nonostante le buone intenzioni, scopri come lavoro con i team di sviluppo PMI sulla code quality e sul processo: le sei abitudini sopra producono risultati concreti già nel primo trimestre di adozione, senza nuovi tool né nuove assunzioni. Se vuoi una valutazione del processo del tuo team con la mappa delle abitudini più carenti e un piano di adozione calibrato sulla maturità attuale, contattami per una consulenza: in due settimane di osservazione ti consegno un assessment, un report con le tre aree di intervento prioritario e metriche di controllo misurabili su tre mesi.

Ultima modifica: