cat swift/laboratorio-dominio-carrello.md

Lezione 06 60 min

Laboratorio: dominio carrello

Esercizio guidato del Modulo 1: costruire un piccolo dominio Swift con prodotti, utenti, carrello, coupon e ordine.

Questo laboratorio chiude il primo modulo. L’obiettivo non è usare tutte le feature possibili di Swift, ma mettere insieme il modello mentale:

  • usare struct per valori di dominio;
  • usare enum per scelte chiuse;
  • usare let e var con intenzione;
  • usare optionals dove l’assenza è significativa;
  • creare tipi piccoli per evitare ambiguità;
  • preferire codice che renda evidenti gli stati validi.

Costruiremo un piccolo dominio: prodotti, utenti, carrello, coupon e ordine.

Requisiti del dominio

Il sistema deve rappresentare:

  • prodotti con nome, categoria e prezzo;
  • righe di carrello con prodotto e quantità;
  • un carrello con più righe e un eventuale coupon;
  • un utente con nome ed email opzionale;
  • un ordine creato a partire dal carrello;
  • un tracking code che può non essere ancora disponibile.

Il dominio deve evitare alcuni errori:

  • prezzo negativo;
  • quantità minore o uguale a zero;
  • sconto percentuale fuori scala;
  • categoria scritta come stringa casuale;
  • uso forzato di valori opzionali.

Passo 1: categorie come enum

Una categoria non dovrebbe essere testo libero.

enum CategoriaProdotto {
  case libri
  case elettronica
  case accessori
  case casa
}

Questo elimina errori come "Accessori", "accessorio", "ACCESSORI" o stringhe vuote.

Passo 2: prezzo come tipo dedicato

Un prezzo non è un semplice Int, anche se internamente può usare centesimi.

struct Prezzo: Equatable {
  let centesimi: Int

  init?(_ centesimi: Int) {
    guard centesimi >= 0 else {
      return nil
    }

    self.centesimi = centesimi
  }
}

L’initializer è fallibile perché un prezzo negativo non deve produrre un valore valido.

Passo 3: quantità valida

struct Quantita: Equatable {
  let valore: Int

  init?(_ valore: Int) {
    guard valore > 0 else {
      return nil
    }

    self.valore = valore
  }
}

Ora non puoi creare una riga con zero prodotti.

Passo 4: prodotto come value type

struct Prodotto: Equatable {
  let id: String
  let nome: String
  let categoria: CategoriaProdotto
  let prezzo: Prezzo
}

Prodotto è una struct perché rappresenta un valore del catalogo. Non ci serve identità condivisa mutabile.

Passo 5: riga carrello

struct RigaCarrello: Equatable {
  let prodotto: Prodotto
  let quantita: Quantita

  var subtotale: Prezzo {
    Prezzo(prodotto.prezzo.centesimi * quantita.valore)!
  }
}

Qui il force unwrap è accettabile? Dipende dal contesto. Il prodotto tra prezzo non negativo e quantità positiva non può essere negativo, quindi l’initializer di Prezzo non dovrebbe fallire.

Tuttavia, questo è un buon punto di discussione: se l’initializer fallibile rende scomodo un calcolo internamente sicuro, forse nel modulo successivo introdurremo initializer privati o metodi factory per modellare meglio l’invariante.

Per ora lo teniamo visibile proprio per imparare a notare il compromesso.

Passo 6: coupon opzionale

struct ScontoPercentuale: Equatable {
  let valore: Int

  init?(_ valore: Int) {
    guard (0...100).contains(valore) else {
      return nil
    }

    self.valore = valore
  }
}

struct Coupon: Equatable {
  let codice: String
  let sconto: ScontoPercentuale
}

Il coupon nel carrello sarà opzionale perché un carrello può esistere senza coupon.

Passo 7: carrello

struct Carrello: Equatable {
  let righe: [RigaCarrello]
  let coupon: Coupon?

  var totaleSenzaSconto: Prezzo {
    let totale = righe.reduce(0) { parziale, riga in
      parziale + riga.subtotale.centesimi
    }

    return Prezzo(totale)!
  }

  var totale: Prezzo {
    guard let coupon else {
      return totaleSenzaSconto
    }

    let riduzione = totaleSenzaSconto.centesimi * coupon.sconto.valore / 100
    return Prezzo(totaleSenzaSconto.centesimi - riduzione)!
  }

  func aggiungendo(_ riga: RigaCarrello) -> Carrello {
    Carrello(righe: righe + [riga], coupon: coupon)
  }

  func applicando(_ coupon: Coupon) -> Carrello {
    Carrello(righe: righe, coupon: coupon)
  }
}

Questo carrello è immutabile: ogni modifica restituisce un nuovo carrello.

Non è l’unico stile possibile, ma è ottimo per imparare value types e testabilità.

Passo 8: utente e ordine

struct Utente: Equatable {
  let id: String
  let nome: String
  let email: String?
}

struct Ordine: Equatable {
  let id: String
  let utente: Utente
  let righe: [RigaCarrello]
  let totale: Prezzo
  let trackingCode: String?
}

email è optional perché un utente può non averla ancora inserita.

trackingCode è optional perché un ordine appena creato potrebbe non essere ancora spedito.

Passo 9: creare un ordine

func creaOrdine(id: String, utente: Utente, carrello: Carrello) -> Ordine? {
  guard !carrello.righe.isEmpty else {
    return nil
  }

  return Ordine(
    id: id,
    utente: utente,
    righe: carrello.righe,
    totale: carrello.totale,
    trackingCode: nil
  )
}

La funzione restituisce Ordine? perché un carrello vuoto non può diventare un ordine valido.

Nel modulo sulla modellazione del dominio vedremo alternative più espressive, per esempio errori tipizzati con throws o Result.

Passo 10: scenario completo

let libro = Prodotto(
  id: "p-1",
  nome: "Manuale Swift",
  categoria: .libri,
  prezzo: Prezzo(2900)!
)

let quantita = Quantita(2)!
let riga = RigaCarrello(prodotto: libro, quantita: quantita)

let coupon = Coupon(
  codice: "SWIFT10",
  sconto: ScontoPercentuale(10)!
)

let carrello = Carrello(righe: [], coupon: nil)
  .aggiungendo(riga)
  .applicando(coupon)

let utente = Utente(
  id: "u-1",
  nome: "Ada",
  email: nil
)

let ordine = creaOrdine(id: "o-1", utente: utente, carrello: carrello)

Ci sono ancora force unwrap negli esempi per non anticipare troppa infrastruttura. Nel codice reale vorremmo gestire quelle creazioni in modo più sicuro. Per questo il prossimo modulo introdurrà modellazione più robusta, errori custom e confini del dominio.

Domande di revisione

Rispondi prima di passare al modulo successivo:

  • quali tipi sono value types e perché?
  • dove abbiamo usato optionals?
  • quali stati invalidi abbiamo reso impossibili?
  • quali stati invalidi sono ancora possibili?
  • dove il codice è espressivo e dove è ancora fragile?

Estensioni consigliate

Se vuoi rendere l’esercizio più completo:

  • impedisci coupon con codice vuoto;
  • aggiungi una categoria digitale;
  • aggiungi un metodo descrizione per Prezzo;
  • evita i force unwrap nei punti in cui crei dati di esempio;
  • crea tre carrelli diversi e confronta i totali;
  • aggiungi una funzione che descrive lo stato del tracking senza usare !.