modulo 19 di 24 / Ore 41-60

Moduli 17-24 / Checkpoint 60 ore

Semplicità prima dell'ottimizzazione

Capire perché la soluzione semplice è quasi sempre quella giusta nel Registro del laboratorio, come misurare prima di ottimizzare, e quando la complessità aggiuntiva è giustificata.

25 min Architettura del software

Semplicità prima dell’ottimizzazione

Questa lezione appartiene al Modulo 19 — Prestazioni e manutenzione.

Il principio e perché esiste

“Semplicità prima dell’ottimizzazione” non significa “scrivi codice lento”. Significa: non aggiungere complessità finché non hai una prova concreta che serve.

Il codice ottimizzato è quasi sempre:

  • più difficile da leggere
  • più difficile da modificare
  • più difficile da testare
  • più soggetto a bug

Se il guadagno di prestazioni non è misurabile, l’ottimizzazione è un costo senza beneficio.

Confronto concreto nel Registro

Versione semplice — ricerca lineare

// Semplice: un array, una funzione filter
function cercaStrumenti(strumenti, { query, categoria, stato }) {
  return strumenti.filter(s => {
    const matchNome = !query || s.nome.toLowerCase().includes(query.toLowerCase());
    const matchCategoria = !categoria || s.categoria === categoria;
    const matchStato = !stato || s.stato === stato;
    return matchNome && matchCategoria && matchStato;
  });
}

Complessità: O(n). Per 50 strumenti: < 0.1 ms. Leggibile, testabile, modificabile.

Versione “ottimizzata” — indice in memoria

// Ottimizzato: indice per ricerca più rapida
class ArchivioStrumentiIndicizzato {
  constructor() {
    this._lista = [];
    this._indiceId = new Map();
    this._indiceCat = new Map();
  }

  salva(strumento) {
    const i = this._lista.findIndex(s => s.id === strumento.id);
    if (i >= 0) this._lista[i] = strumento;
    else this._lista.push(strumento);
    this._indiceId.set(strumento.id, strumento);
    const catList = this._indiceCat.get(strumento.categoria) ?? [];
    this._indiceCat.set(strumento.categoria, [...catList.filter(s => s.id !== strumento.id), strumento]);
  }

  trovaPerId(id) { return this._indiceId.get(id) ?? null; }
  leggiTutti() { return [...this._lista]; }
  // ... più codice per mantenere gli indici aggiornati
}

Complessità: O(1) per ID, O(k) per categoria. Più veloce con 10.000 strumenti. Ma: più righe, più difficile da testare, bug più sottili.

Per un laboratorio scolastico con 50 strumenti: la versione semplice è corretta. La versione ottimizzata è sbagliata — aggiunge complessità senza beneficio misurabile.

Come misurare prima di ottimizzare

Se pensi di aver trovato un collo di bottiglia, misura prima di cambiare:

// Strumento 1: console.time / console.timeEnd
console.time("cerca-strumenti");
const risultati = cercaStrumenti(strumenti, { query: "trapano" });
console.timeEnd("cerca-strumenti");
// Output: "cerca-strumenti: 0.123 ms" → non è un problema

// Strumento 2: performance.now per misure più precise
const inizio = performance.now();
const risultati = cercaStrumenti(strumenti, { query: "trapano" });
const fine = performance.now();
console.log(`Ricerca: ${(fine - inizio).toFixed(3)} ms con ${strumenti.length} strumenti`);

Soglie pratiche:

< 1 ms   → non è un problema — non ottimizzare
1-10 ms  → monitorare — ottimizzare solo se cresce
10-100 ms → problema reale per operazioni frequenti — valutare ottimizzazione
> 100 ms → problema evidente — ottimizzare con priorità

Tre esempi di ottimizzazione prematura nel Registro

Esempio 1: indice per ID quando si ha un array piccolo

// ❌ Ottimizzazione prematura
const indice = new Map(strumenti.map(s => [s.id, s])); // build O(n)
const trovato = indice.get(id); // lookup O(1)

// ✅ Soluzione semplice — identica prestazione per N < 1000
const trovato = strumenti.find(s => s.id === id); // O(n) ma < 0.1 ms

Esempio 2: cache per filtraggio quando i dati cambiano raramente

// ❌ Ottimizzazione prematura — complessità per dati che cambiano
let cacheRisultati = null;
let cacheQuery = null;

function cercaStrumentiCached(strumenti, query) {
  if (query === cacheQuery && cacheRisultati) return cacheRisultati;
  cacheRisultati = strumenti.filter(s => s.nome.includes(query));
  cacheQuery = query;
  return cacheRisultati;
}
// Problema: la cache non si invalida quando uno strumento cambia stato

// ✅ Soluzione semplice — ricalcola sempre, < 1 ms per 50 strumenti
function cercaStrumenti(strumenti, query) {
  return strumenti.filter(s => s.nome.includes(query));
}

Esempio 3: lazy loading di localStorage per “prestazioni”

// ❌ Ottimizzazione prematura — localStorage è già veloce
let datiCaricati = false;
let datiCache = null;

function leggiTutti() {
  if (!datiCaricati) {
    datiCache = JSON.parse(localStorage.getItem("strumenti") || "[]");
    datiCaricati = true;
  }
  return datiCache;
}
// Problema: la cache diventa stale se i dati cambiano

// ✅ Soluzione semplice — leggi ogni volta, è < 1 ms
function leggiTutti() {
  return JSON.parse(localStorage.getItem("strumenti") || "[]");
}

Quando l’ottimizzazione è giustificata

Ottimizza quando:
✓ hai misurato e il problema è reale (> 10 ms per operazioni frequenti)
✓ il numero di elementi crescerà significativamente oltre il contesto attuale
✓ l'ottimizzazione non riduce la leggibilità del codice

Non ottimizzare quando:
✗ "penso che potrebbe essere lento"
✗ il numero di elementi è stabile e piccolo
✗ l'ottimizzazione rende il codice più difficile da capire e testare

Procedura guidata

  1. Identifica la parte del Registro che pensi possa essere lenta.
  2. Aggiungi console.time attorno all’operazione.
  3. Usa il Registro normalmente — guarda il tempo nel log.
  4. Se è sotto 10 ms: lascia la soluzione semplice e documenta la decisione.
  5. Se è sopra 10 ms: valuta un’ottimizzazione mirata e documentala.

Attività in classe

## Analisi semplicità vs ottimizzazione

Operazione analizzata:

Soluzione attuale (semplice):

Tempo misurato con console.time:

Conclusione:
  [ ] Non è un problema — lascio la soluzione semplice
  [ ] È un problema — valuto ottimizzazione perché [motivo con misura]

Se ho lasciato la soluzione semplice, documento la decisione:
  "Ho scelto di non ottimizzare [operazione] perché il tempo è [X ms]
   con [N] elementi — accettabile per il contesto del laboratorio scolastico."

Spiegazione guidata per studiare

La complessità del codice ha un costo reale: tempo per capirlo, rischio di bug, difficoltà nel modificarlo. Questo costo va pagato ogni volta che qualcuno tocca il codice. Un’ottimizzazione prematura fa pagare quel costo ogni giorno, anche quando il problema che voleva risolvere non esiste.

Il punto centrale è: la soluzione semplice è sempre il punto di partenza — non il punto di arrivo temporaneo. Si parte dalla versione più leggibile e si ottimizza solo quando la misura lo richiede.

Il rischio tipico è ottimizzare per ansia — “e se un giorno ci sono 10.000 strumenti?”. Questa ansia porta a costruire infrastrutture per problemi futuri ipotetici, pagando il costo oggi. La disciplina sta nel rispondere: “quando ci saranno 10.000 strumenti, misuro e ottimizo. Adesso ho 50 strumenti.”

Passo per passo nel progetto

  1. Apri application/cercaStrumenti.js — usa .filter() o qualcosa di più complesso?
  2. Aggiungi console.time("cerca") prima e console.timeEnd("cerca") dopo.
  3. Apri il Registro, filtra per nome — guarda il tempo.
  4. È sotto 1 ms? Rimuovi il console.time e non ottimizzare.
  5. Documenta nel README: “la ricerca usa filter() su array — accettabile per N strumenti”.

Esempio da leggere lentamente

// Scelta sbagliata: ottimizzazione senza misura
// Prima misura: 0.04 ms con 30 strumenti
// Dopo ottimizzazione: 0.03 ms con 30 strumenti
// Guadagno: 0.01 ms — invisibile, ma il codice è ora più complesso e fragile

// Scelta giusta: mantieni il semplice, documenta la decisione
function cercaStrumenti(strumenti, query) {
  return strumenti.filter(s => s.nome.toLowerCase().includes(query.toLowerCase()));
}

// Nel README:
// "La ricerca usa filter() — O(n) ma < 0.1 ms per fino a 500 strumenti.
//  Da ottimizzare solo se il laboratorio supera i 500 strumenti."

Prova di comprensione

  • Hai misurato qualcosa nel Registro con console.time? Qual era il risultato?
  • Con quanti strumenti il .filter() diventerebbe lento nel browser?
  • Quale ottimizzazione faresti se il laboratorio avesse 10.000 strumenti?

Errori da evitare

  • ottimizzare senza misurare — il problema potrebbe non esistere;
  • considerare “potrebbe essere lento un giorno” come giustificazione sufficiente;
  • aggiungere cache o indici per dati che cambiano frequentemente — la cache diventa stale;
  • rendere il codice illeggibile per guadagni di pochi millisecondi invisibili all’utente.

Prodotto da consegnare

Può essere:

  • la misurazione con console.time di un’operazione del Registro, documentata;
  • una nota nel README che spiega perché la soluzione semplice è stata preferita;
  • l’analisi di un caso in cui si è valutata un’ottimizzazione e si è deciso di non farla.

Checklist di chiusura

  • Ho misurato almeno un’operazione con console.time
  • Ho confrontato il tempo misurato con la soglia pratica (10 ms)
  • Ho documentato la decisione di mantenere la soluzione semplice (o di ottimizzare)
  • La decisione nomina il numero di elementi e il tempo misurato

Domande per la revisione

  • Il tempo misurato giustificava un’ottimizzazione?
  • Hai trovato un caso in cui la soluzione semplice era già più veloce di quanto pensavi?
  • Dove nel tuo progetto potresti avere un collo di bottiglia reale se i dati crescessero?

Risultato atteso

Alla fine lo studente deve saper dire:

Ho misurato [operazione] — il tempo è [X ms] con [N] strumenti.
Ho deciso di [mantenere la soluzione semplice / ottimizzare] perché [motivo con misura].
La decisione è documentata nel README.

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