modulo 17 di 24 / Ore 41-60

Moduli 17-24 / Checkpoint 60 ore

Mapping tra dati esterni e dominio

Scrivere funzioni di mapping che trasformano i dati ricevuti da localStorage, file JSON o API nel modello di dominio del Registro, e viceversa.

30 minArchitettura del software

Mapping tra dati esterni e dominio

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

Il problema del formato esterno

Quando i dati vengono salvati in localStorage, in un file JSON o restituiti da un’API, spesso hanno un formato diverso dal modello di dominio. Le differenze più comuni:

  • Nomi delle proprietà diversi — l’API usa name, il dominio usa nome
  • Tipi diversi — l’API restituisce date come stringhe ISO, il dominio potrebbe usare oggetti Date
  • Campi extra — l’API restituisce campi che il dominio non usa
  • Campi mancanti — un dato salvato con una versione precedente non ha tutti i campi

Il mapping è la funzione che traduce tra i due formati. Sta nell’Infrastructure — non nel dominio, non nel caso d’uso.

Mapping da formato esterno a dominio

Scenario: un’API restituisce strumenti con nomi in inglese. Il dominio usa italiano.

// Dati ricevuti dall'API (formato esterno)
const dtoApiStrumento = {
  id: "str-042",
  name: "Electric drill",        // inglese
  category: "Electric",          // inglese
  status: "available",           // inglese
  last_updated: "2024-01-15T10:30:00Z"  // campo extra non usato dal dominio
};

// Funzione di mapping — sta nell'adapter, non nel dominio
function dtoApiToStrumento(dto) {
  const statoMap = {
    "available": "disponibile",
    "on_loan": "in_prestito",
    "damaged": "guasto",
    "maintenance": "in_manutenzione"
  };

  return {
    id: dto.id,
    nome: dto.name,
    categoria: dto.category,
    stato: statoMap[dto.status] ?? "disponibile"
    // last_updated ignorato — il dominio non lo usa
  };
}

Mapping da dominio a formato esterno

Quando si salva, la direzione si inverte:

// Funzione di mapping inversa — da dominio a DTO per localStorage/API
function strumentoToDto(strumento) {
  const statoMap = {
    "disponibile": "available",
    "in_prestito": "on_loan",
    "guasto": "damaged",
    "in_manutenzione": "maintenance"
  };

  return {
    id: strumento.id,
    name: strumento.nome,
    category: strumento.categoria,
    status: statoMap[strumento.stato] ?? "available"
  };
}

Mapping con localStorage (formato interno)

Quando si usa localStorage il formato è spesso quasi identico al dominio — ma non sempre. Se il dominio è cambiato nel tempo, potrebbero esserci dati salvati con la versione vecchia.

// Mapping da localStorage con gestione dati mancanti o vecchi
function dtoLocalStorageToStrumento(dto) {
  return {
    id: dto.id ?? crypto.randomUUID(),          // campo mancante → genera nuovo id
    nome: dto.nome ?? dto.name ?? "Senza nome", // vecchio formato con "name"
    categoria: dto.categoria ?? "Non classificato",
    stato: ["disponibile", "in_prestito", "guasto", "in_manutenzione"].includes(dto.stato)
      ? dto.stato
      : "disponibile"                            // stato non valido → default disponibile
  };
}

Dove stanno le funzioni di mapping

infrastructure/
  archivioStrumentiLocalStorage.js   ← usa dtoLocalStorageToStrumento internamente
  archivioStrumentiAPI.js            ← usa dtoApiToStrumento internamente
  mapping/
    strumentoMapping.js              ← (opzionale) funzioni di mapping separate

Le funzioni di mapping non escono dall’adapter. Il caso d’uso non chiama mai dtoApiToStrumento direttamente — lo fa l’adapter internamente.

// Dentro l'adapter API — il caso d'uso non vede mai il DTO
async function trovaPerId(id) {
  const risposta = await fetch(`/api/strumenti/${id}`);
  if (!risposta.ok) throw new Error("ARCHIVIO_NON_DISPONIBILE");
  const dto = await risposta.json();
  return dtoApiToStrumento(dto); // ← mapping interno all'adapter
}

Gestire campi mancanti e dati corrotti

Un mapping robusto non lancia eccezioni per dati parziali — usa valori di default sensati:

function dtoToStrumento(dto) {
  if (!dto || typeof dto !== "object") return null; // dati completamente corrotti

  return {
    id: typeof dto.id === "string" && dto.id.length > 0
      ? dto.id
      : null,  // null = non usabile, verrà filtrato
    nome: typeof dto.nome === "string" && dto.nome.trim().length > 0
      ? dto.nome.trim()
      : "Senza nome",
    categoria: typeof dto.categoria === "string"
      ? dto.categoria
      : "Non classificato",
    stato: ["disponibile", "in_prestito", "guasto", "in_manutenzione"].includes(dto.stato)
      ? dto.stato
      : "disponibile"
  };
}

// Nell'adapter: filtra gli oggetti non validi
function leggiTutti() {
  const raw = JSON.parse(localStorage.getItem("registro:strumenti") || "[]");
  return raw
    .map(dtoToStrumento)
    .filter(s => s !== null && s.id !== null);
}

Procedura guidata

  1. Identifica il formato esterno dei tuoi dati (localStorage, file JSON, API).
  2. Elenca le differenze rispetto al modello di dominio (nomi, tipi, campi extra/mancanti).
  3. Scrivi dtoToStrumento che gestisce i casi mancanti con valori di default.
  4. Scrivi strumentoToDto per la direzione inversa.
  5. Usa le funzioni solo dentro l’adapter — mai nei casi d’uso o nel dominio.
  6. Testa con un DTO incompleto: il mapping produce un oggetto usabile?

Attività in classe

## Mapping nel mio Registro

Formato esterno usato (localStorage / JSON / API):

Differenze rispetto al modello di dominio:

Funzione dtoToStrumento scritta (sì/no):
  - Gestisce campo mancante: sì/no
  - Gestisce stato non valido: sì/no

Funzione strumentoToDto scritta (sì/no):

Le funzioni stanno nell'adapter (sì/no):

Test con DTO incompleto: risultato:

Spiegazione guidata per studiare

Il mapping è il punto di traduzione tra due “lingue”: il formato del sistema esterno e il modello del dominio. Senza mapping, il formato esterno entra nel dominio e il dominio comincia a conoscere dettagli tecnici che non lo riguardano.

Il punto centrale è: il mapping sta nell’adapter, non nel dominio. Il dominio parla solo di Strumento con nome, categoria e stato. Non sa che fuori esiste un’API che usa name, category e status. Questa ignoranza è intenzionale e va protetta.

Il rischio tipico è usare direttamente l’oggetto ricevuto dall’API nel dominio. Funziona finché l’API non cambia formato. Quando l’API aggiorna un campo da status a current_status, il dominio si rompe. Con il mapping, si aggiorna solo la funzione dtoApiToStrumento — il dominio non sa che è successo nulla.

Passo per passo nel progetto

  1. Apri l’adapter localStorage (o API) del tuo progetto.
  2. Cerca dove vengono letti i dati: vengono usati direttamente nel caso d’uso o trasformati prima?
  3. Se non esiste, crea dtoToStrumento con gestione dei campi mancanti.
  4. Verifica che il caso d’uso riceva sempre un oggetto nel formato del dominio — mai il DTO grezzo.
  5. Testa: cambia il nome di un campo nel DTO salvato in localStorage — il Registro funziona ancora?

Esempio da leggere lentamente

// Adapter con mapping interno — il caso d'uso riceve sempre Strumento
function creaArchivioStrumentiLocalStorage() {
  const CHIAVE = "registro:strumenti";

  function leggiTutti() {
    try {
      const raw = JSON.parse(localStorage.getItem(CHIAVE) || "[]");
      return raw.map(dtoToStrumento).filter(s => s && s.id);
      // ↑ mapping qui dentro — il caso d'uso non vede mai il DTO grezzo
    } catch {
      return [];
    }
  }

  function salva(strumento) {
    try {
      const lista = leggiTutti();
      const dto = strumentoToDto(strumento); // ← mapping inverso prima di salvare
      const i = lista.findIndex(s => s.id === strumento.id);
      if (i >= 0) lista[i] = dto; else lista.push(dto);
      localStorage.setItem(CHIAVE, JSON.stringify(lista));
    } catch {
      throw new Error("ARCHIVIO_NON_DISPONIBILE");
    }
  }

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

Prova di comprensione

  • Se l’API cambia il campo status in current_status, quale file devi modificare?
  • Perché il dominio non deve mai ricevere un DTO grezzo dall’API?
  • Come gestisci un DTO con un campo stato che contiene un valore sconosciuto?

Errori da evitare

  • usare direttamente l’oggetto ricevuto da fetch() nel caso d’uso senza mapping;
  • mettere le funzioni di mapping in domain/ — appartengono a infrastructure/;
  • non gestire i campi mancanti — dati vecchi in localStorage causeranno crash;
  • fare il mapping parzialmente (solo da esterno a dominio, dimenticando la direzione inversa).

Prodotto da consegnare

Può essere:

  • dtoToStrumento e strumentoToDto scritte e usate nell’adapter;
  • un test con un DTO incompleto che verifica il comportamento del mapping;
  • una nota nel README che descrive il formato esterno e le differenze col dominio.

Checklist di chiusura

  • Esiste una funzione che trasforma il DTO nel modello di dominio
  • Esiste una funzione inversa per la direzione dominio → DTO
  • Le funzioni stanno nell’adapter, non nel dominio
  • I campi mancanti producono valori di default sensati, non errori
  • Il caso d’uso riceve sempre un oggetto nel formato del dominio

Domande per la revisione

  • Dove nel tuo progetto avviene la trasformazione dal formato salvato al formato del dominio?
  • Se cambiassi il nome di un campo nel DTO, quanti file dovresti modificare?
  • Il tuo adapter gestisce i dati corrotti o vecchi senza bloccare il Registro?

Risultato atteso

Alla fine lo studente deve saper dire:

Il formato esterno del mio Registro differisce dal dominio per: [differenze].
La funzione dtoToStrumento gestisce i campi mancanti restituendo [valori di default].
Il mapping avviene in [file/adapter] — il caso d'uso non vede mai il DTO grezzo.
Ho verificato con [test/scenario] che il mapping funziona anche con dati incompleti.

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