modulo 05 / lezione 25

Correggere senza regressioni

Fix minimo e fix cosmetico

Separare la correzione necessaria da refactor e pulizie che rischiano di confondere la verifica.

45 minDebugging

In parole semplici

Quando trovi il bug, le mani prudono. Quel if annidato è brutto, quel nome di variabile è terribile, quella funzione è troppo lunga. Resistere è la prima regola del fix: il fix corregge il bug, niente di più. Tutto il resto va in un altro diff.

Mescolare fix e refactor è il modo più rapido per introdurre regressioni mentre risolvi un bug.

Parole nuove

  • fix minimo: la modifica più piccola che corregge il bug e non altro.
  • fix cosmetico: modifiche di forma (naming, whitespace, riordino) non strettamente necessarie.
  • refactor: ristrutturazione del codice senza cambiarne il comportamento esterno.
  • diff: differenza tra due versioni del codice (git diff).

Cosa impari

  • Distinguere ciò che corregge da ciò che ripulisce.
  • Dividere un cambiamento misto in due commit puliti.
  • Capire perché la disciplina protegge la verificabilità.

La regola della singola intenzione

Ogni diff dovrebbe rispondere a una sola domanda:

  • “Cosa corregge questo diff?”
  • oppure: “Cosa rinomina/refattora questo diff?”
  • oppure: “Cosa aggiunge questo diff?”

Mai due risposte in uno solo. Quando rivedi il diff prima del commit, se devi rispondere “fix più rinomina più ordinamento”, separa.

Perché conta

1. La verifica è ambigua

Se hai cambiato 30 righe (5 fix + 25 cosmetiche) e il test passa, non sai se passa perché il fix funziona o perché qualcosa di cosmetico ha cambiato comportamento per caso.

2. Il revert è più difficile

Il fix dimostra un problema: voglio revertarlo. Ma è mescolato a un refactor utile. Non posso revertare un pezzo solo.

3. La code review affoga

Il revisore deve guardare 30 righe per capire le 5 importanti. Si distrae e approva male.

4. git blame mente

Tra sei mesi, qualcuno fa git blame su una riga importante e trova “rinominato variabile”. Il fix originale è perso nella storia.

Esempio: lo stesso fix in due varianti

Bug: calcolaSconto ritorna NaN quando percentuale è 0.

Variante A: fix minimo

 function calcolaSconto(prezzo, percentuale) {
-  return prezzo - (prezzo * percentuale / 100);
+  return prezzo - (prezzo * (percentuale || 0) / 100);
 }

Una riga. Si vede subito: “abbiamo aggiunto || 0 per gestire null/undefined”.

Variante B: fix + refattorizzazione (sbagliata)

-function calcolaSconto(prezzo, percentuale) {
-  return prezzo - (prezzo * percentuale / 100);
+function calcolaSconto(originalPrice, discountPercent = 0) {
+  if (originalPrice <= 0) return 0;
+  const discount = (originalPrice * discountPercent) / 100;
+  return originalPrice - discount;
 }

Cinque righe. Tutto sembra un miglioramento. Ma:

  • Hai cambiato il nome dei parametri (potresti rompere chi chiama).
  • Hai introdotto un controllo <= 0 non richiesto dal bug.
  • Hai cambiato un’espressione algebricamente equivalente, ma per il lettore è un’altra cosa.

Se domani un test fallisce, qual è il colpevole?

La pratica: due commit, due intenzioni

Per separare:

# 1. fix minimo
git add -p          # aggiungi solo le righe del fix
git commit -m "fix(sconto): gestire percentuale null/undefined"

# 2. refactor
git add -p          # aggiungi solo le righe del refactor
git commit -m "refactor(sconto): rinominare parametri e ristrutturare guard"

Due commit. Due diff puliti. Due revert possibili.

Quando il refactor è dentro il fix

Eccezione legittima: a volte il bug è un problema strutturale che richiede ristrutturazione minima per essere fixato. Esempio: la funzione legge da uno stato globale che viene aggiornato in ordine sbagliato; per fixare devi passare lo stato come parametro.

In quel caso il refactor è necessario al fix, e va nel commit del fix con un commit message che lo spiega:

fix(carrello): passare cart come parametro a calcolaTotale

Il bug era causato dalla lettura di `cart` globale prima dell'update.
Il fix richiede di passare il cart come parametro esplicito, eliminando
la dipendenza dallo stato globale per quella funzione.

Quando il refactor è invece opportunismo (“già che ci sono”), va separato.

Esercizio guidato

Apri una repo dove hai un piccolo bug da risolvere (o creane uno).

  1. Risolvi il bug mescolando fix e qualche pulizia. Vedi il diff.
  2. git reset e ricomincia. Questa volta:
    • Prima fix puro.
    • Commit.
    • Refactor.
    • Commit.
  3. Confronta i due percorsi: quale ti renderebbe più sicuro al merge?

Tecnica: git add -p per separare

git add -p divide le modifiche in hunk e per ognuno chiede se aggiungerlo. Ti permette di costruire un commit con solo certe righe lasciando le altre fuori.

Comandi rapidi:

  • y aggiungi
  • n salta
  • s dividi ancora
  • e modifica manuale
  • q esci

Errori frequenti del principiante

  • “Già che ci sono”. Ogni “già che ci sono” è una regressione che aspetta.
  • Commit “WIP fix + rinomine + cleanup”. Tre intenzioni, nessuna verificabile.
  • Refactor senza test. Il refactor sembra “ovvio” ma può cambiare comportamento.
  • Saltare git add -p. Senza, è troppo facile committare tutto in fretta.