cat swift/associated-values-stati-ricchi.md

Lezione 0845 min

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.