modulo 04 di 24 / Ore 0-20

Moduli 1-8 / Checkpoint 20 ore

Separare lettura input, decisione, salvataggio e messaggio

Riorganizzare il flusso di un'azione del Registro distinguendo input della UI, decisione applicativa, persistenza e messaggio all'utente.

30 minArchitettura del software

Separare lettura input, decisione, salvataggio e messaggio

Nel Registro fragile molte azioni seguono sempre lo stesso schema:

  1. leggere input;
  2. decidere che cosa fare;
  3. modificare dati;
  4. salvare;
  5. mostrare un messaggio.

Quando questi passaggi sono mescolati nello stesso blocco, il codice diventa difficile da leggere e da cambiare. Quando invece li separi, anche restando nello stesso file, il flusso diventa più chiaro.

Questa lezione è un ponte: non siamo ancora alla struttura completa con presentation, application, domain e infrastructure, ma iniziamo a vedere quei confini dentro una singola azione.

Obiettivo della lezione

Alla fine della lezione devi saper:

  • dividere un’azione del Registro in passaggi riconoscibili;
  • capire quali passaggi appartengono alla UI e quali no;
  • evitare che la UI decida regole importanti;
  • evitare che il salvataggio sia sparso ovunque;
  • trasformare una funzione principale in un flusso leggibile;
  • spiegare quali limiti restano dopo questa prima separazione.

Il flusso da rendere visibile

Per RegistraPrestito possiamo scrivere:

UI:
  leggi id strumento e nome studente

Decisione:
  verifica che lo strumento esista
  verifica che lo studente sia presente
  verifica che lo strumento sia prestabile

Modifica:
  cambia stato
  crea prestito

Salvataggio:
  salva strumenti e prestiti

Messaggio:
  comunica successo o errore

Questo non è ancora un caso d’uso completo, ma è già un flusso più leggibile. La differenza è importante: un caso d’uso vero dovrà vivere fuori dalla UI; qui stiamo preparando il terreno.

Il punto non è creare tante funzioni per fare bella figura. Il punto è dare un nome a ogni responsabilità, così quando il codice cresce possiamo spostare i pezzi nel livello giusto.

Prima: una funzione che fa tutto

Partiamo da una versione concentrata in un solo blocco:

function registraPrestito() {
  const strumentoId = inputStrumentoId.value.trim();
  const studente = inputStudente.value.trim();
  const strumento = strumenti.find((item) => item.id === strumentoId);

  if (!strumento) {
    messaggioElement.textContent = "Strumento non trovato";
    return;
  }

  if (!studente) {
    messaggioElement.textContent = "Inserisci il nome dello studente";
    return;
  }

  if (strumento.stato === "guasto") {
    messaggioElement.textContent = "Uno strumento guasto non può essere prestato";
    return;
  }

  if (strumento.stato === "in_prestito") {
    messaggioElement.textContent = "Lo strumento è già in prestito";
    return;
  }

  strumento.stato = "in_prestito";

  prestiti.push({
    id: `pre-${Date.now()}`,
    strumentoId: strumento.id,
    studente,
    aperto: true,
  });

  localStorage.setItem("strumenti", JSON.stringify(strumenti));
  localStorage.setItem("prestiti", JSON.stringify(prestiti));

  mostraStrumenti(strumenti);
  messaggioElement.textContent = "Prestito registrato";
}

Il comportamento è chiaro solo finché la funzione resta piccola. Appena aggiungiamo altre regole, altri stati, un salvataggio diverso o messaggi più curati, il blocco diventa una stanza piena di cavi.

Lo scomponiamo in quattro domande:

1. Da dove arrivano i dati?
2. Quale decisione deve prendere il sistema?
3. Quali dati cambiano?
4. Che cosa deve vedere l'utente?

Separare la lettura input

La UI legge input:

function leggiInputPrestito() {
  return {
    strumentoId: inputStrumentoId.value.trim(),
    studente: inputStudente.value.trim(),
  };
}

Questa funzione può dipendere dal DOM, perché il suo lavoro è proprio leggere la UI.

Il confine è chiaro:

  • prende valori da elementi HTML;
  • pulisce gli spazi;
  • restituisce un oggetto semplice.

Non deve:

  • cercare lo strumento;
  • decidere se lo strumento è prestabile;
  • salvare dati;
  • mostrare messaggi.

Una buona lettura input produce un piccolo comando:

{
  strumentoId: "s-12",
  studente: "Giulia Rossi"
}

Questo oggetto è più maneggevole dei campi HTML. Può essere passato ad altre funzioni e, più avanti, a un caso d’uso.

Separare la decisione

Creiamo una funzione che decide se l’operazione puo procedere:

function validaRichiestaPrestito({ strumento, studente }) {
  if (!strumento) {
    return {
      ok: false,
      codice: "strumento_non_trovato",
      messaggio: "Strumento non trovato",
    };
  }

  if (!studente) {
    return {
      ok: false,
      codice: "studente_mancante",
      messaggio: "Inserisci il nome dello studente",
    };
  }

  if (strumento.stato === "guasto") {
    return {
      ok: false,
      codice: "strumento_guasto",
      messaggio: "Uno strumento guasto non può essere prestato",
    };
  }

  if (strumento.stato === "in_prestito") {
    return {
      ok: false,
      codice: "strumento_gia_in_prestito",
      messaggio: "Lo strumento è già in prestito",
    };
  }

  return { ok: true };
}

Questa funzione è ancora didatticamente intermedia: restituisce anche messaggi utente. Più avanti separeremo errore e messaggio in modo più netto.

Per ora abbiamo già ottenuto un miglioramento: la decisione non è più nascosta in mezzo a lettura input, salvataggio e render.

La funzione risponde a una domanda precisa:

questa richiesta di prestito può procedere?

Se la risposta è no, restituisce un motivo.

Separare ricerca e decisione

Attenzione: cercare lo strumento e decidere se è prestabile non sono la stessa cosa.

La ricerca recupera un dato:

function trovaStrumentoPerId(strumenti, id) {
  return strumenti.find((strumento) => strumento.id === id);
}

La decisione interpreta quel dato:

const validazione = validaRichiestaPrestito({
  strumento,
  studente: input.studente,
});

Questa separazione sembra piccola, ma aiuta a leggere il flusso:

  • prima recupero ciò che mi serve;
  • poi valuto se posso procedere.

Se un giorno lo strumento verrà letto da un database, la ricerca cambierà. La regola “uno strumento guasto non può essere prestato” non dovrebbe cambiare.

Separare la modifica dati

La modifica puo essere isolata:

function applicaPrestito(strumento, studente) {
  strumento.stato = "in_prestito";

  const prestito = {
    id: `pre-${Date.now()}`,
    strumentoId: strumento.id,
    studente,
    aperto: true,
  };

  prestiti.push(prestito);

  return {
    strumento,
    prestito,
  };
}

Anche questa funzione non è definitiva: usa prestiti globale. Ma isola il momento in cui cambiano i dati.

Possiamo renderla più esplicita passando anche la lista dei prestiti:

function applicaPrestito(strumento, prestiti, studente) {
  strumento.stato = "in_prestito";

  const prestito = {
    id: `pre-${Date.now()}`,
    strumentoId: strumento.id,
    studente,
    aperto: true,
  };

  prestiti.push(prestito);

  return prestito;
}

Non è ancora perfetta, perché modifica oggetti e array esistenti. Però adesso è più chiaro quale passaggio cambia lo stato del sistema.

Regola pratica:

una funzione che modifica dati deve avere un nome che lo dichiara.

applicaPrestito è più chiaro di gestisci, submit, ok o fai.

Separare salvataggio

function salvaRegistro() {
  localStorage.setItem("strumenti", JSON.stringify(strumenti));
  localStorage.setItem("prestiti", JSON.stringify(prestiti));
}

La funzione è ancora accoppiata a localStorage, ma almeno il salvataggio ha un nome.

Meglio ancora:

function salvaRegistro(strumentiDaSalvare, prestitiDaSalvare) {
  localStorage.setItem("strumenti", JSON.stringify(strumentiDaSalvare));
  localStorage.setItem("prestiti", JSON.stringify(prestitiDaSalvare));
}

Questa funzione appartiene concettualmente all’infrastruttura, anche se per ora resta nello stesso file. Il suo lavoro non è decidere regole, ma trasformare dati in JSON e salvarli.

Più avanti potremo sostituirla con:

salvaRegistroSuJson(strumenti, prestiti);

oppure:

await salvaRegistroSuApi(strumenti, prestiti);

Se il resto del flusso non dipende dai dettagli di localStorage, il cambiamento sarà molto meno doloroso.

Separare messaggio

function mostraMessaggio(testo) {
  messaggioElement.textContent = testo;
}

Questa funzione sembra piccola, ma rende esplicito che comunicare con l’utente è una responsabilità della UI.

La UI può mostrare:

  • messaggi di successo;
  • errori comprensibili;
  • indicazioni su cosa fare dopo;
  • stati vuoti, come “Nessuno strumento trovato”.

La UI non dovrebbe essere l’unico luogo in cui esiste una regola. Può tradurre un errore in messaggio, ma non dovrebbe decidere da sola se uno strumento guasto può essere prestato.

Un passaggio intermedio può essere una piccola funzione di traduzione:

function messaggioPerErrorePrestito(codice) {
  if (codice === "strumento_non_trovato") {
    return "Strumento non trovato";
  }

  if (codice === "studente_mancante") {
    return "Inserisci il nome dello studente";
  }

  if (codice === "strumento_guasto") {
    return "Uno strumento guasto non può essere prestato";
  }

  if (codice === "strumento_gia_in_prestito") {
    return "Lo strumento è già in prestito";
  }

  return "Operazione non riuscita";
}

Questa versione è un po’ più lunga, ma prepara una separazione utile: il sistema può restituire un codice, la UI sceglie il testo da mostrare.

Il flusso completo

Ora registraPrestito puo diventare:

function registraPrestito() {
  const input = leggiInputPrestito();
  const strumento = trovaStrumentoPerId(strumenti, input.strumentoId);

  const validazione = validaRichiestaPrestito({
    strumento,
    studente: input.studente,
  });

  if (!validazione.ok) {
    mostraMessaggio(validazione.messaggio);
    return;
  }

  applicaPrestito(strumento, input.studente);
  salvaRegistro(strumenti, prestiti);
  mostraStrumenti(strumenti);
  mostraMessaggio("Prestito registrato");
}

Questa funzione è ancora nello stesso file, ma ora racconta una storia:

leggi input
trova dati
valida
applica
salva
mostra

La funzione principale non contiene tutti i dettagli. Coordina i passaggi. Questo è un primo assaggio di ciò che farà il livello Application: non disegna la pagina, non salva direttamente, non contiene tutti i dettagli del dominio, ma coordina un’operazione.

Perche questo e utile

Non abbiamo ancora introdotto cartelle, classi o pattern.

Abbiamo solo dato nomi alle responsabilita.

Questo porta vantaggi immediati:

  • il flusso principale e piu leggibile;
  • alcune funzioni sono piu facili da testare manualmente;
  • le regole iniziano ad avere un punto;
  • il salvataggio e localizzato;
  • i messaggi sono meno sparsi;
  • i prossimi refactoring saranno piu semplici.

Limiti da riconoscere

Non fingiamo che sia tutto risolto.

Restano limiti:

  • validaRichiestaPrestito mescola regole e messaggi;
  • applicaPrestito modifica dati globali;
  • salvaRegistro dipende direttamente da localStorage;
  • registraPrestito conosce ancora troppe funzioni concrete;
  • non ci sono test automatici.

Ma ora sappiamo meglio dove intervenire.

Schema mentale da usare in ogni azione

Ogni volta che lavori su un’azione del Registro, prova a compilare questa tabella:

| Passaggio | Domanda | Esempio nel prestito | Dove dovrebbe stare in futuro |
| --- | --- | --- | --- |
| Lettura input | Che cosa ha inserito l'utente? | id strumento, nome studente | Presentation |
| Decisione | L'operazione è permessa? | strumento esiste, non è guasto | Application + Domain |
| Modifica | Quali dati cambiano? | stato in_prestito, nuovo prestito | Domain/Application |
| Salvataggio | Dove salvo? | localStorage | Infrastructure |
| Messaggio | Che cosa vede l'utente? | "Prestito registrato" | Presentation |

Questa tabella serve a non perdersi. Se non sai dove mettere una riga di codice, chiediti quale passaggio rappresenta.

Esempio rapido: aprire una segnalazione di guasto

Lo stesso schema vale per un’altra azione.

Flusso:

leggi input:
  id strumento
  descrizione guasto

decisione:
  lo strumento esiste?
  la descrizione è presente?
  lo strumento è già guasto?

modifica:
  cambia stato in guasto
  crea segnalazione

salvataggio:
  salva strumenti e segnalazioni

messaggio:
  mostra esito

Possibile funzione principale:

function apriSegnalazioneGuasto() {
  const input = leggiInputGuasto();
  const strumento = trovaStrumentoPerId(strumenti, input.strumentoId);
  const validazione = validaSegnalazioneGuasto({ strumento, descrizione: input.descrizione });

  if (!validazione.ok) {
    mostraMessaggio(validazione.messaggio);
    return;
  }

  applicaSegnalazioneGuasto(strumento, segnalazioni, input.descrizione);
  salvaRegistro(strumenti, prestiti, segnalazioni);
  mostraStrumenti(strumenti);
  mostraMessaggio("Segnalazione registrata");
}

Non è il codice finale del progetto. È una forma didattica: rende visibile il percorso dell’azione.

Attivita guidata

Riorganizza registraPrestito seguendo questa sequenza:

leggi input
trova strumento
valida
applica modifica
salva
mostra messaggio
renderizza

Per ogni passaggio crea una funzione con un nome esplicito.

Poi rispondi:

  • quale funzione dipende dalla UI?
  • quale funzione contiene regole?
  • quale funzione salva?
  • quale funzione modifica dati?
  • quale funzione sarebbe piu facile testare?

Micro-sfida

Applica lo stesso schema al cambio stato.

Obiettivo:

  • trasformare una funzione confusa in un flusso leggibile.

Consegna:

  • scegli l’azione “cambia stato”;
  • separa lettura input, decisione, modifica, salvataggio e messaggio;
  • lascia tutto nello stesso file, se serve;
  • aggiungi una nota che spiega quale passaggio sarà spostato in futuro.

Vincoli:

  • separa lettura input;
  • separa validazione;
  • separa modifica;
  • separa salvataggio;
  • separa messaggio;
  • non creare ancora cartelle nuove.

Risultato atteso:

function cambiaStatoStrumento() {
  const input = leggiInputCambioStato();
  const strumento = trovaStrumentoPerId(strumenti, input.strumentoId);
  const validazione = validaCambioStato({ strumento, nuovoStato: input.nuovoStato });

  if (!validazione.ok) {
    mostraMessaggio(validazione.messaggio);
    return;
  }

  applicaCambioStato(strumento, input.nuovoStato);
  salvaRegistro(strumenti, prestiti);
  mostraStrumenti(strumenti);
  mostraMessaggio("Stato aggiornato");
}

Il codice può essere diverso, ma deve raccontare lo stesso flusso.

Prodotto da ottenere

Alla fine della lezione devi avere:

  • registraPrestito riorganizzata in passaggi leggibili;
  • almeno cinque funzioni con responsabilità nominate;
  • una tabella che collega passaggio e livello futuro;
  • la micro-sfida sul cambio stato;
  • una verifica manuale del comportamento prima e dopo.

Esempio di verifica:

| Caso | Prima | Dopo |
| --- | --- | --- |
| Prestito valido | ok | ok |
| Strumento non trovato | ok | ok |
| Studente mancante | ok | ok |
| Strumento guasto | ok | ok |
| Strumento già in prestito | ok | ok |

Domande di comprensione

Rispondi per iscritto:

  • Perché leggere input e decidere una regola sono responsabilità diverse?
  • Quale funzione può conoscere il DOM?
  • Quale funzione può conoscere localStorage?
  • Perché è utile restituire un oggetto { ok: false, codice, messaggio } invece di scrivere subito nella pagina?
  • Quale passaggio assomiglia di più a un futuro caso d’uso?
  • Quale passaggio assomiglia di più a un futuro adapter di infrastruttura?

Controllo finale

La lezione è completata quando puoi dire sì a queste domande:

  • ho separato almeno quattro responsabilità nel flusso di prestito;
  • il comportamento del programma non è cambiato;
  • so indicare quale funzione legge la UI;
  • so indicare quale funzione decide;
  • so indicare quale funzione salva;
  • so indicare quale funzione comunica con l’utente;
  • ho riconosciuto i limiti ancora presenti.

Nel prossimo passo cercheremo le dipendenze nascoste: variabili globali, DOM implicito e funzioni che sembrano semplici ma dipendono da mezzo programma.