cat swift/laboratorio-refactor-swift-idiomatico.md
Laboratorio: refactor Swift idiomatico
Trasformare codice funzionante ma fragile in Swift idiomatico: tipi, enum, API leggibili, collection pipeline e testabilità.
Questo laboratorio chiude il modulo su Swift idiomatico. Partiamo da codice che funziona, ma è brutto: stringhe magiche, parametri primitivi, nomi generici, logica procedurale e fallimenti poveri.
L’obiettivo è trasformarlo in codice che sembri Swift: chiaro, tipizzato, vicino al dominio.
Codice di partenza
func applyCoupon(type: String, value: Int, total: Int, categories: [String], isFirst: Bool) -> Int {
if type == "percent" {
if isFirst == true {
return total - (total * value / 100)
}
return total
}
if type == "fixed" {
if categories.contains("books") {
return total - value
}
return total
}
return total
}
Questo codice ha molti problemi:
typeè una stringa magica;valuecambia significato;totalè unIntsenza dominio;categoriesusa stringhe;isFirstè poco chiaro;- non sappiamo perché un coupon non viene applicato;
- il risultato può diventare negativo;
- la funzione fa troppe cose.
Passo 1: nominare il dominio
enum CategoriaProdotto: String, Codable {
case libri
case elettronica
case accessori
}
struct Prezzo: Equatable, Comparable {
let centesimi: Int
init?(_ centesimi: Int) {
guard centesimi >= 0 else {
return nil
}
self.centesimi = centesimi
}
static func < (lhs: Prezzo, rhs: Prezzo) -> Bool {
lhs.centesimi < rhs.centesimi
}
}
Ora prezzo e categoria non sono più valori primitivi anonimi.
Passo 2: modellare lo sconto
enum Sconto: Equatable {
case percentuale(Int)
case importoFisso(Prezzo)
}
Il tipo dello sconto ora determina il significato del valore.
Passo 3: contesto esplicito
struct ContestoCoupon: Equatable {
let totale: Prezzo
let categorie: Set<CategoriaProdotto>
let utenteHaGiaOrdinato: Bool
}
Il contesto raccoglie ciò che serve alla validazione. La funzione non riceve più cinque parametri scollegati.
Passo 4: regole come enum
enum RegolaCoupon: Equatable {
case primoOrdineSolo
case categoriaRichiesta(CategoriaProdotto)
}
struct Coupon: Equatable {
let codice: String
let sconto: Sconto
let regole: [RegolaCoupon]
}
Il coupon è ora un valore di dominio.
Passo 5: errori espliciti
enum ErroreCoupon: Error, Equatable {
case nonPrimoOrdine
case categoriaMancante(CategoriaProdotto)
}
Non basta restituire il totale invariato: vogliamo sapere perché.
Passo 6: motore leggibile
struct CouponEngine {
func applica(_ coupon: Coupon, nel contesto: ContestoCoupon) throws -> Prezzo {
try valida(coupon, nel: contesto)
return totaleApplicando(coupon.sconto, a: contesto.totale)
}
}
La chiamata ora legge bene:
let totale = try engine.applica(coupon, nel: contesto)
Passo 7: validazione con pattern matching
extension CouponEngine {
func valida(_ coupon: Coupon, nel contesto: ContestoCoupon) throws {
for regola in coupon.regole {
try valida(regola, nel: contesto)
}
}
func valida(_ regola: RegolaCoupon, nel contesto: ContestoCoupon) throws {
switch regola {
case .primoOrdineSolo:
guard contesto.utenteHaGiaOrdinato == false else {
throw ErroreCoupon.nonPrimoOrdine
}
case .categoriaRichiesta(let categoria):
guard contesto.categorie.contains(categoria) else {
throw ErroreCoupon.categoriaMancante(categoria)
}
}
}
}
Lo switch parla il linguaggio del dominio.
Passo 8: calcolo dello sconto
private extension CouponEngine {
func totaleApplicando(_ sconto: Sconto, a totale: Prezzo) -> Prezzo {
switch sconto {
case .percentuale(let valore):
let riduzione = totale.centesimi * valore / 100
return Prezzo(max(0, totale.centesimi - riduzione))!
case .importoFisso(let importo):
return Prezzo(max(0, totale.centesimi - importo.centesimi))!
}
}
}
Il force unwrap è tollerabile qui solo perché max(0, ...) garantisce un valore non negativo. In un refactor ulteriore potremmo aggiungere un initializer interno non fallibile per valori già validati.
Passo 9: usare collection pipeline con giudizio
Se hai più coupon e vuoi trovare quelli applicabili:
let couponApplicabili = couponDisponibili.filter { coupon in
do {
try engine.valida(coupon, nel: contesto)
return true
} catch {
return false
}
}
Qui filter è leggibile. Ma se vuoi conservare gli errori, non usare filter: perderesti informazione.
let risultati = couponDisponibili.map { coupon in
Result {
try engine.applica(coupon, nel: contesto)
}
}
La scelta dipende dall’obiettivo.
Passo 10: confronto finale
Prima:
applyCoupon(type: "percent", value: 10, total: 6000, categories: ["books"], isFirst: true)
Dopo:
let coupon = Coupon(
codice: "WELCOME10",
sconto: .percentuale(10),
regole: [.primoOrdineSolo, .categoriaRichiesta(.libri)]
)
let contesto = ContestoCoupon(
totale: Prezzo(6000)!,
categorie: [.libri],
utenteHaGiaOrdinato: false
)
let totale = try engine.applica(coupon, nel: contesto)
La seconda versione è più lunga, ma molto più chiara e sicura.
Checklist del refactor
Verifica di aver migliorato:
- stringhe magiche sostituite da enum;
- primitive sostituite da tipi di dominio dove serve;
- nomi leggibili;
- fallimenti espliciti;
- switch esaustivi;
- funzioni piccole;
- comportamento vicino ai tipi;
- nessun protocollo inutile;
- pipeline usate solo dove chiariscono.
Esercizio finale
Estendi il refactor:
- aggiungi
.spedizioneGratuita; - aggiungi regola
.minimoCarrello(Prezzo); - crea
CodiceCouponvalidato; - raccogli tutti gli errori invece di fermarti al primo;
- crea una funzione che ordina i coupon dal più conveniente al meno conveniente.
Poi scrivi una breve retrospettiva: quale parte del codice è diventata più lunga, e perché vale la pena?