modulo 17 di 24 / Ore 41-60

Moduli 17-24 / Checkpoint 60 ore

Definire cosa deve restare invariato

Identificare le parti del Registro che non devono cambiare quando si sostituisce l'infrastruttura: dominio, casi d'uso e contratti tra livelli.

30 min Architettura del software

Definire cosa deve restare invariato

Questa lezione appartiene al Modulo 17 — Variante database o API.

Il problema

Vuoi sostituire localStorage con un’API reale. Oppure passi da un file JSON a un database SQLite. Oppure cambi solo la struttura dei dati salvati.

La domanda è: cosa devi riscrivere?

Se il progetto è ben strutturato, la risposta è: solo l’infrastruttura. Dominio e casi d’uso non si toccano. Se invece il progetto ha le responsabilità mescolate, cambiare l’archivio significa riscrivere tutto.

Questa lezione ti aiuta a capire, prima di fare il cambio, cosa deve restare fermo e perché.

Cosa deve restare invariato

Il dominio

Le funzioni del dominio descrivono le regole del laboratorio. Non dipendono da come i dati vengono salvati.

Queste funzioni non devono cambiare quando cambia l’infrastruttura:

// domain/strumento.js — resta invariato qualunque sia l'archivio
function creaStrumento({ id, nome, categoria }) {
  if (!id) throw new Error("ID_MANCANTE");
  if (!nome || nome.trim().length < 2) throw new Error("NOME_NON_VALIDO");
  return { id, nome: nome.trim(), categoria, stato: "disponibile" };
}

function puoEsserePrestato(strumento) {
  return strumento.stato === "disponibile";
}

Se stai cambiando l’infrastruttura e ti trovi a modificare creaStrumento o puoEsserePrestato, hai trovato una dipendenza nascosta da correggere prima.

I casi d’uso

I casi d’uso coordinano le operazioni. Ricevono archivi come dipendenze — non li costruiscono internamente.

// application/registraPrestito.js — resta invariato
function creaRegistraPrestito({ archivioStrumenti, archivioPrestiti }) {
  return async function registraPrestito(comando) {
    const strumento = await archivioStrumenti.trovaPerId(comando.strumentoId);
    if (!strumento) throw new Error("STRUMENTO_NON_TROVATO");
    if (!puoEsserePrestato(strumento)) throw new Error("STRUMENTO_NON_DISPONIBILE");

    const prestito = apriPrestito(strumento.id, new Date().toISOString());
    await archivioPrestiti.salva({ ...prestito, assegnatario: comando.assegnatario });
    await archivioStrumenti.salva({ ...strumento, stato: "in_prestito" });
  };
}

Questo caso d’uso funziona con localStorage, con un’API, con un file JSON o con un archivio in memoria per i test. Non sa quale archivio riceve — parla solo con il contratto.

Il contratto dell’archivio

Il contratto è l’insieme dei metodi che un archivio deve avere. In TypeScript sarebbe un’interfaccia; in JavaScript è semplicemente la forma attesa:

// Contratto atteso da archivioStrumenti — resta invariato
// trovaPerId(id: string) → Strumento | null
// salva(strumento: Strumento) → void
// leggiTutti() → Strumento[]

Finché il tuo nuovo archivio rispetta questo contratto, il caso d’uso non si accorge del cambiamento.

Cosa può cambiare

Solo il livello Infrastructure. L’adapter che parla con localStorage:

// infrastructure/archivioStrumentiLocalStorage.js — QUESTO cambia
function creaArchivioStrumentiLocalStorage() {
  const CHIAVE = "registro:strumenti";

  function leggiTutti() {
    try {
      return JSON.parse(localStorage.getItem(CHIAVE) || "[]");
    } catch {
      return [];
    }
  }

  function trovaPerId(id) {
    return leggiTutti().find(s => s.id === id) ?? null;
  }

  function salva(strumento) {
    const lista = leggiTutti();
    const indice = lista.findIndex(s => s.id === strumento.id);
    if (indice >= 0) lista[indice] = strumento;
    else lista.push(strumento);
    localStorage.setItem(CHIAVE, JSON.stringify(lista));
  }

  return { leggiTutti, trovaPerId, salva };
}

Puoi sostituire questo con un adapter che chiama fetch("/api/strumenti") — e il caso d’uso registraPrestito non sa nulla del cambio.

Come verificare che il confine regge

Il test più diretto: esegui il caso d’uso con due archivi diversi e verifica che il comportamento sia identico.

// test: stesso caso d'uso, due archivi — entrambi devono passare
const archivioMemoria = creaArchivioStrumentiInMemoria();
const archivioLS = creaArchivioStrumentiLocalStorage();

const registraConMemoria = creaRegistraPrestito({
  archivioStrumenti: archivioMemoria,
  archivioPrestiti: creaArchivioPrestiti()
});

const registraConLS = creaRegistraPrestito({
  archivioStrumenti: archivioLS,
  archivioPrestiti: creaArchivioPrestiti()
});

// Entrambi devono lanciare lo stesso errore con uno strumento inesistente

Se entrambi si comportano allo stesso modo, il dominio è davvero indipendente dall’infrastruttura.

Procedura guidata

  1. Prima di cambiare archivio, elenca per iscritto: dominio, casi d’uso, contratti — questi non si toccano.
  2. Identifica il nuovo adapter da scrivere (il file che cambia).
  3. Verifica che il nuovo adapter rispetti il contratto atteso (stessi metodi, stessa forma dei dati restituiti).
  4. Esegui i test esistenti con il nuovo adapter — devono passare senza modifiche ai casi d’uso.
  5. Se un test fallisce, il problema è nell’adapter, non nel dominio.

Attività in classe

Lavora su una scheda breve:

## Definire cosa deve restare invariato

Archivio attuale:

Archivio nuovo (o ipotetico):

Contratto che entrambi devono rispettare (metodi e firme):

Cosa ho verificato che non cambia:

Limite rimasto:

Spiegazione guidata per studiare

Quando cambi l’infrastruttura, il dominio e i casi d’uso devono restare identici. Questo non è un obiettivo astratto: è verificabile eseguendo gli stessi test con un archivio diverso.

Per definire cosa deve restare invariato, il punto centrale è: il caso d’uso non costruisce l’archivio — lo riceve. Questa separazione è ciò che rende possibile sostituire localStorage con un’API senza toccare le regole del laboratorio.

Il rischio tipico è scrivere un caso d’uso che chiama localStorage.getItem direttamente. In quel momento il caso d’uso e l’infrastruttura sono accoppiati — cambiare uno significa cambiare l’altro. La soluzione è spostare quella chiamata in un adapter e passare l’adapter come dipendenza.

Passo per passo nel progetto

  1. Apri i file dei casi d’uso (application/). Cercano localStorage, fetch, JSON.parse direttamente? Se sì, c’è una dipendenza da estrarre.
  2. Elenca i metodi che il caso d’uso chiama sull’archivio (trovaPerId, salva, leggiTutti).
  3. Scrivi un adapter alternativo che rispetta quegli stessi metodi.
  4. Sostituisci l’archivio nel punto di composizione (main.js) — solo lì.
  5. Verifica che il comportamento del Registro sia identico.

Esempio da leggere lentamente

// main.js — il SOLO punto dove si sceglie l'archivio
import { creaArchivioStrumentiLocalStorage } from "./infrastructure/archivioStrumentiLocalStorage.js";
import { creaArchivioStrumentiAPI } from "./infrastructure/archivioStrumentiAPI.js";
import { creaRegistraPrestito } from "./application/registraPrestito.js";

// Cambia solo questa riga per passare da localStorage a API:
const archivioStrumenti = creaArchivioStrumentiLocalStorage();
// const archivioStrumenti = creaArchivioStrumentiAPI();  ← basta decommentare

const registraPrestito = creaRegistraPrestito({ archivioStrumenti, archivioPrestiti });

Nota: registraPrestito non cambia. creaArchivioStrumentiAPI deve avere gli stessi metodi di creaArchivioStrumentiLocalStorage.

Prova di comprensione

  • Elenca tre cose del Registro che non devono cambiare quando cambia l’archivio.
  • Cosa significa che un archivio “rispetta il contratto”?
  • Se il tuo caso d’uso smette di funzionare dopo aver cambiato archivio, dove cerchi il problema?

Errori da evitare

  • chiamare localStorage o fetch direttamente dentro un caso d’uso;
  • costruire l’archivio dentro il caso d’uso con new o chiamate dirette;
  • modificare il dominio o i casi d’uso quando si cambia solo l’archivio;
  • non verificare che il nuovo adapter rispetti il contratto prima di collegarlo.

Prodotto da consegnare

Può essere:

  • un secondo adapter scritto (es. archivioStrumentiInMemoria.js) che rispetta lo stesso contratto;
  • una nota tecnica che elenca cosa resta invariato e cosa cambia;
  • un test che esegue lo stesso caso d’uso con due archivi diversi.

Checklist di chiusura

  • Il dominio non importa localStorage, fetch o dettagli di database
  • I casi d’uso ricevono l’archivio come dipendenza, non lo costruiscono
  • Il nuovo adapter ha gli stessi metodi del precedente
  • Il comportamento del Registro è identico con entrambi gli archivi
  • Il punto di composizione (main.js) è l’unico posto dove si sceglie l’archivio

Domande per la revisione

  • Hai trovato dipendenze nascoste nei casi d’uso? Dove stavano?
  • Il contratto dell’archivio è scritto da qualche parte (commento, tipo, test)?
  • Che cosa cambieresti nel progetto per rendere ancora più facile la sostituzione?

Risultato atteso

Alla fine lo studente deve saper dire:

Nel mio Registro, queste parti restano invariate quando cambia l'archivio: [elenco].
Il contratto che l'archivio deve rispettare ha questi metodi: [lista].
Ho verificato che [caso d'uso] funziona con [archivio A] e con [archivio B].

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