cat swift/laboratorio-dominio-carrello.md
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
structper valori di dominio; - usare
enumper scelte chiuse; - usare
letevarcon 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
descrizioneperPrezzo; - 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
!.