modulo 14 di 24 / Ore 21-40

Moduli 9-16 / Checkpoint 40 ore

Refactoring: cambiare struttura senza cambiare comportamento

Capire cosa significa refactoring, perché è diverso da riscrivere, e applicarlo al Registro del laboratorio con piccoli passi verificabili.

25 min Architettura del software

Refactoring: cambiare struttura senza cambiare comportamento

Questa lezione appartiene al Modulo 14 — Refactoring: migliorare senza ricominciare.

Definizione operativa

Refactoring = cambiare la struttura interna del codice senza cambiare il comportamento osservabile dall’esterno.

Non è refactoring:

  • aggiungere una funzionalità nuova
  • correggere un bug
  • riscrivere tutto da capo perché “il codice fa schifo”

È refactoring:

  • estrarre una funzione lunga in funzioni più piccole con nomi precisi
  • spostare una regola dal posto sbagliato al posto giusto
  • rinominare una variabile o funzione per comunicare meglio l’intenzione
  • separare responsabilità mischiate in un solo posto

Esempio concreto nel Registro — prima e dopo

Prima: tutto in un event listener

// presentation/ui.js — PRIMA del refactoring
// Un event listener che fa troppe cose
document.getElementById("btn-presta").addEventListener("click", () => {
  // 1. Legge input
  const strumentoId = document.getElementById("strumento-id").value.trim();
  const assegnatario = document.getElementById("assegnatario").value.trim();

  // 2. Valida
  if (!strumentoId) { alert("ID strumento obbligatorio"); return; }

  // 3. Cerca lo strumento nell'archivio (dipendenza diretta!)
  const strumenti = JSON.parse(localStorage.getItem("registro:strumenti") || "[]");
  const strumento = strumenti.find(s => s.id === strumentoId);
  if (!strumento) { alert("Strumento non trovato"); return; }

  // 4. Applica la regola di dominio (nel posto sbagliato!)
  if (strumento.stato !== "disponibile") { alert("Non disponibile"); return; }

  // 5. Modifica e salva
  strumento.stato = "in_prestito";
  const i = strumenti.findIndex(s => s.id === strumentoId);
  strumenti[i] = strumento;
  localStorage.setItem("registro:strumenti", JSON.stringify(strumenti));

  alert("Prestito registrato!");
});

Dopo: ogni responsabilità al suo posto

// domain/strumento.js
function puoEsserePrestato(strumento) {
  if (strumento.stato !== "disponibile") throw new Error("STRUMENTO_NON_DISPONIBILE");
}

// application/registraPrestito.js
function creaRegistraPrestito({ archivioStrumenti, archivioPrestiti }) {
  return async function registraPrestito(comando) {
    const strumento = await archivioStrumenti.trovaPerId(comando.strumentoId);
    if (!strumento) throw new Error("STRUMENTO_NON_TROVATO");
    puoEsserePrestato(strumento);
    const prestito = apriPrestito(strumento.id, new Date().toISOString());
    await archivioPrestiti.salva({ ...prestito, assegnatario: comando.assegnatario });
    await archivioStrumenti.salva({ ...strumento, stato: "in_prestito" });
  };
}

// presentation/ui.js
document.getElementById("btn-presta").addEventListener("click", async () => {
  const comando = {
    strumentoId: document.getElementById("strumento-id").value.trim(),
    assegnatario: document.getElementById("assegnatario").value.trim()
  };
  try {
    await registraPrestito(comando);
    mostraSuccesso("Prestito registrato.");
  } catch (errore) {
    mostraErrore(messaggi[errore.message] ?? "Operazione non riuscita.");
  }
});

Il comportamento per l’utente è identico. La struttura è completamente diversa — e molto più manutenibile.

I passi del refactoring

Il refactoring non si fa tutto in una volta. Si fa in passi piccoli, ognuno verificabile:

Passo 1: scrivi un test (o uno scenario manuale) che descrive il comportamento attuale
Passo 2: identifica una sola responsabilità da spostare
Passo 3: estrai quella responsabilità in una funzione separata
Passo 4: verifica che il comportamento sia invariato
Passo 5: (opzionale) commit — il progetto funziona, il codice è migliorato
Passo 6: ripeti per la prossima responsabilità

Regola fondamentale: dopo ogni passo, il Registro deve funzionare. Non si accumula debito tecnico “temporaneo” tra un passo e l’altro.

Tre tipi di refactoring nel Registro

Tipo 1: estrazione di funzione

Quando una funzione fa troppo, si estrae una parte in una funzione con nome preciso.

// Prima: logica di filtraggio inline nell'event listener
const risultati = strumenti.filter(s =>
  s.nome.toLowerCase().includes(query) &&
  (categoria === "" || s.categoria === categoria)
);

// Dopo: funzione estratta con nome che comunica l'intenzione
function filtraStrumenti(strumenti, { query, categoria }) {
  return strumenti.filter(s =>
    s.nome.toLowerCase().includes(query.toLowerCase()) &&
    (categoria === "" || s.categoria === categoria)
  );
}

Tipo 2: spostamento di responsabilità

Quando una responsabilità è nel posto sbagliato, si sposta nel livello corretto.

// Prima: regola del dominio nell'event listener
if (strumento.stato === "guasto") { alert("Guasto"); return; }

// Dopo: regola nel dominio
// domain/strumento.js
function puoEsserePrestato(strumento) {
  if (strumento.stato !== "disponibile") throw new Error("STRUMENTO_NON_DISPONIBILE");
}

Tipo 3: rendere esplicita una dipendenza

Quando una funzione usa qualcosa di nascosto, si rende visibile nella firma.

// Prima: dipendenza nascosta da localStorage
function salvaStrumento(strumento) {
  const lista = JSON.parse(localStorage.getItem("registro:strumenti") || "[]");
  // ...
}

// Dopo: dipendenza esplicita — l'archivio viene passato
function salvaStrumento(archivio, strumento) {
  const lista = archivio.leggiTutti();
  // ...
}

Come verificare che il comportamento non sia cambiato

Tre metodi, in ordine di affidabilità:

  1. Test automatico — scrivi il test prima del refactoring, eseguilo dopo
  2. Scenario manuale documentato — esegui esattamente gli stessi passi prima e dopo
  3. Confronto del diff — leggi le righe cambiate e verifica che nessuna modifica la logica

Il metodo 1 è il più affidabile. Il metodo 3 è il meno affidabile ma spesso sufficiente per refactoring piccoli (es. rinomino).

Procedura guidata

  1. Scegli un punto del Registro dove il codice fa troppo (evento listener, funzione lunga).
  2. Scrivi in una frase il comportamento che deve restare invariato dopo il refactoring.
  3. Esegui una verifica prima di iniziare (test o scenario manuale).
  4. Estrai una responsabilità — solo una, non tutte insieme.
  5. Verifica che il comportamento sia invariato.
  6. Ripeti per la prossima responsabilità.

Attività in classe

## Refactoring eseguito

Punto del codice scelto (file e funzione):

Problema identificato:

Tipo di refactoring: estrazione / spostamento / dipendenza esplicita

Verifica eseguita prima:

Descrizione della modifica:

Verifica eseguita dopo:

Comportamento cambiato (sì/no — deve essere no):

Prossima responsabilità da estrarre (se esiste):

Spiegazione guidata per studiare

Il refactoring è un’abilità disciplinata — non è riscrivere perché non piace il codice. La disciplina sta nei passi: uno alla volta, verifica in mezzo, mai rompere il comportamento osservabile.

Il punto centrale è: dopo un refactoring, l’utente non deve accorgersi di nulla. Se la UI mostra un comportamento diverso, non è un refactoring — è una modifica funzionale. Le due cose vanno separate.

Il rischio tipico è fare refactoring e aggiungere una funzionalità nello stesso passo. Questo rende impossibile capire se un eventuale problema viene dal refactoring o dalla nuova funzionalità. Separali sempre in commit distinti.

Passo per passo nel progetto

  1. Apri l’event listener del form di registrazione prestito.
  2. Quante responsabilità ci sono? (Leggi input, valida, chiama archivio, applica regola, salva, mostra messaggio)
  3. Scegli una sola da estrarre — la più isolata.
  4. Estrai la funzione. Verifica che il Registro funzioni identicamente.
  5. Documenta in una nota: “estratto [X] da [Y] — comportamento invariato”.

Esempio da leggere lentamente

// Refactoring in tre passi — ogni passo lascia il programma funzionante

// PASSO 1: estrai la lettura dell'input
function leggiComandoPrestito() {
  return {
    strumentoId: document.getElementById("strumento-id").value.trim(),
    assegnatario: document.getElementById("assegnatario").value.trim()
  };
}
// Verifica: il form funziona ancora ✓

// PASSO 2: estrai la gestione dell'errore
function gestisciErrorePrestito(errore) {
  const messaggi = {
    STRUMENTO_NON_TROVATO: "Strumento non trovato.",
    STRUMENTO_NON_DISPONIBILE: "Strumento non disponibile."
  };
  mostraErrore(messaggi[errore.message] ?? "Operazione non riuscita.");
}
// Verifica: gli errori funzionano ancora ✓

// PASSO 3: l'event listener diventa leggibile
document.getElementById("btn-presta").addEventListener("click", async () => {
  const comando = leggiComandoPrestito();
  try {
    await registraPrestito(comando);
    mostraSuccesso("Prestito registrato.");
  } catch (errore) {
    gestisciErrorePrestito(errore);
  }
});
// Verifica: tutto funziona ancora ✓

Tre passi, tre verifiche, nessuna modifica al comportamento.

Prova di comprensione

  • Qual è la differenza tra refactoring e riscrittura?
  • Perché si fa un passo alla volta invece di cambiare tutto insieme?
  • Come verifichi che il comportamento non è cambiato dopo il refactoring?

Errori da evitare

  • fare refactoring e aggiungere funzionalità nello stesso passo;
  • non verificare dopo ogni passo — il bug si accumula senza saperlo;
  • riscrivere una funzione da zero invece di modificarla incrementalmente;
  • considerare concluso il refactoring quando “sembra giusto” invece che quando i test passano.

Prodotto da consegnare

Può essere:

  • un diff del codice prima e dopo il refactoring, con descrizione della responsabilità estratta;
  • la scheda compilata con verifica prima e dopo;
  • un commit che contiene solo refactoring (nessuna funzionalità aggiunta).

Checklist di chiusura

  • Ho identificato una responsabilità da estrarre
  • Ho verificato il comportamento prima di iniziare
  • Ho estratto la responsabilità in una funzione con nome preciso
  • Ho verificato che il comportamento sia identico dopo
  • Il Registro funziona correttamente dalla UI

Domande per la revisione

  • Quante responsabilità ha ancora la funzione che hai refactorizzato?
  • Il comportamento per l’utente è identico a prima?
  • Hai aggiunto funzionalità durante il refactoring? (Se sì, separa in un altro commit.)

Risultato atteso

Alla fine lo studente deve saper dire:

Ho estratto [responsabilità] da [funzione/file].
Prima: [descrizione dello stato originale].
Dopo: [descrizione del nuovo stato].
Il comportamento per l'utente è identico — ho verificato con [metodo].

Se questa spiegazione non è possibile, la lezione non è ancora davvero conclusa.