cat swift/associated-values-stati-ricchi.md
Associated values e stati ricchi
Gli enum Swift non sono semplici liste di costanti: possono portare dati e rappresentare stati del dominio con precisione.
In molti linguaggi, un enum è poco più di una lista di nomi. In Swift, un enum può rappresentare alternative che contengono dati diversi. Questi dati si chiamano associated values.
Sono fondamentali per la modellazione del dominio perché permettono di dire: questi stati sono alternativi, e ciascuno porta solo le informazioni che gli servono.
Il problema degli stati piatti
Immagina un pagamento:
struct Pagamento {
let stato: String
let transactionId: String?
let motivoErrore: String?
}
Questo modello permette stati incoerenti:
let pagamento = Pagamento(
stato: "completato",
transactionId: nil,
motivoErrore: "Carta rifiutata"
)
Un pagamento completato senza transazione e con motivo di errore non ha senso, ma il modello lo consente.
Stato come enum
Un primo miglioramento:
enum StatoPagamento {
case nonIniziato
case inElaborazione
case completato
case fallito
}
Meglio di una stringa, ma mancano ancora i dati specifici: un pagamento completato dovrebbe avere un id transazione; uno fallito dovrebbe avere un motivo.
Associated values
enum StatoPagamento {
case nonIniziato
case inElaborazione
case completato(transactionId: String)
case fallito(motivo: String)
}
Ora gli stati portano solo i dati coerenti.
let ok = StatoPagamento.completato(transactionId: "tx-123")
let ko = StatoPagamento.fallito(motivo: "Carta rifiutata")
Non puoi creare un pagamento completato senza transactionId. Non puoi creare un fallimento senza motivo. Il tipo protegge il dominio.
Coupon come stati ricchi
Nel motore di regole useremo coupon di tipo diverso:
enum Sconto {
case percentuale(Int)
case importoFisso(Int)
case spedizioneGratuita
}
Già così evitiamo un modello debole come:
struct Sconto {
let tipo: String
let valore: Int?
}
Nel modello debole, valore è opzionale perché non tutti i tipi lo usano. Nel modello con enum, il valore esiste solo nei casi che lo richiedono.
Dati etichettati
Quando gli associated values non sono autoevidenti, usa etichette:
enum RegolaCoupon {
case minimoCarrello(cent: Int)
case categoriaRichiesta(CategoriaProdotto)
case primoOrdineSolo
case validoPerUtente(id: String)
}
Le etichette rendono il codice più leggibile:
let regola = RegolaCoupon.minimoCarrello(cent: 5000)
Stati impossibili
Associated values servono soprattutto a eliminare stati impossibili.
Modello fragile:
struct Spedizione {
let gratuita: Bool
let costoInCent: Int?
}
Possibile stato incoerente:
Spedizione(gratuita: true, costoInCent: 499)
Modello migliore:
enum Spedizione {
case gratuita
case aPagamento(costoInCent: Int)
}
Ora non puoi dire contemporaneamente gratuita e a pagamento.
Enum annidati nel dominio
Puoi creare enum piccoli, vicini al concetto che descrivono.
struct Coupon {
let codice: String
let sconto: Sconto
let regole: [RegolaCoupon]
}
Il coupon non è un insieme di flag. È un oggetto composto da concetti espliciti:
- un codice;
- uno sconto;
- una lista di regole.
Quando non usare associated values
Non tutti gli enum devono portare dati.
enum CategoriaProdotto {
case libri
case elettronica
case accessori
}
Qui non servono associated values. Le categorie sono solo alternative chiuse.
Usali quando ogni caso ha bisogno di dati propri.
Attenzione ai casi troppo grandi
Un enum può diventare troppo carico:
enum StatoCheckout {
case carrello(prodotti: [Prodotto], coupon: Coupon?, totale: Int)
case pagamento(utente: Utente, metodo: String, tentativi: Int)
case completato(ordine: Ordine, ricevuta: String, tracking: String?)
}
Non è sempre sbagliato, ma se i dati crescono molto potresti voler creare tipi dedicati:
struct DatiPagamento {
let utente: Utente
let metodo: MetodoPagamento
let tentativi: Int
}
enum StatoCheckout {
case carrello(Carrello)
case pagamento(DatiPagamento)
case completato(Ordine)
}
Il principio resta lo stesso: ogni stato porta i dati che gli appartengono.
Esercizio
Modella questi concetti usando enum con associated values:
Sconto: percentuale, importo fisso, spedizione gratuita;RegolaCoupon: minimo carrello, categoria richiesta, primo ordine, utente specifico;StatoPagamento: non iniziato, in elaborazione, completato, fallito;Spedizione: gratuita o a pagamento.
Poi scrivi per ogni enum uno stato impossibile che il tuo modello impedisce.