cat swift/generics-riuso-significato.md
Generics per riuso senza perdere significato
I generics non sono magia: servono a scrivere codice riusabile mantenendo tipi forti e intenzioni chiare.
I generics permettono di scrivere codice che funziona con più tipi senza perdere sicurezza. In Swift sono ovunque: Array<Element>, Dictionary<Key, Value>, Result<Success, Failure>, Optional<Wrapped>.
Il punto non è creare astrazioni sofisticate. Il punto è evitare duplicazione quando il comportamento è davvero lo stesso.
Il problema della duplicazione
Immagina di voler rappresentare una risposta paginata:
struct PaginaProdotti {
let elementi: [Prodotto]
let paginaCorrente: Int
let totalePagine: Int
}
struct PaginaOrdini {
let elementi: [Ordine]
let paginaCorrente: Int
let totalePagine: Int
}
La struttura è identica. Cambia solo il tipo degli elementi.
Tipo generico
struct Pagina<Element> {
let elementi: [Element]
let paginaCorrente: Int
let totalePagine: Int
}
Ora puoi scrivere:
let prodotti: Pagina<Prodotto>
let ordini: Pagina<Ordine>
Il tipo resta forte. Una Pagina<Prodotto> non è una Pagina<Ordine>.
Nomi generici leggibili
Swift usa spesso nomi come Element, Key, Value, Success, Failure. Evita lettere singole quando il ruolo non è ovvio.
struct Cache<Key, Value> {
private var storage: [Key: Value] = [:]
}
Qui Key e Value sono chiari. In un dominio potresti usare:
struct Validazione<Input> {
let input: Input
}
Il nome del parametro generico deve aiutare a leggere il concetto.
Constraints
A volte un generic deve richiedere capacità specifiche.
struct Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
subscript(key: Key) -> Value? {
get { storage[key] }
set { storage[key] = newValue }
}
}
Dictionary richiede chiavi Hashable. Quindi anche la nostra cache lo richiede.
Funzioni generiche
func primo<Element>(in elementi: [Element]) -> Element? {
elementi.first
}
Questa funzione funziona con qualsiasi array:
let primoProdotto = primo(in: prodotti)
let primoOrdine = primo(in: ordini)
Ma non creare funzioni generiche se non aggiungono valore. array.first esiste già.
Generics nel dominio: regole
Supponiamo di voler rappresentare una regola che valida un input:
struct Regola<Input> {
let descrizione: String
let valida: (Input) -> Bool
}
Possiamo usarla per coupon:
struct ContestoCoupon {
let totaleCarrelloInCent: Int
let utenteHaGiaOrdinato: Bool
}
let minimoCarrello = Regola<ContestoCoupon>(
descrizione: "Carrello minimo 50 euro",
valida: { contesto in
contesto.totaleCarrelloInCent >= 5000
}
)
Questa astrazione ha senso se avremo più regole dello stesso tipo.
Generic non significa vago
Questo è troppo generico:
struct Box<T> {
let value: T
}
Non è sbagliato tecnicamente, ma nel dominio non dice nulla.
Meglio usare generics quando il tipo ha un ruolo:
struct Identificato<ID: Hashable, Valore> {
let id: ID
let valore: Valore
}
Oppure:
struct RisultatoValidazione<Input> {
let input: Input
let errori: [String]
var valido: Bool {
errori.isEmpty
}
}
Associated types nei protocol
Un protocollo può avere un tipo associato:
protocol Validatore {
associatedtype Input
func valida(_ input: Input) -> Bool
}
Implementazione:
struct ValidatoreCoupon: Validatore {
func valida(_ input: ContestoCoupon) -> Bool {
input.totaleCarrelloInCent > 0
}
}
Gli associated types sono potenti, ma aumentano complessità. In un corso base/intermedio vanno usati quando risolvono un problema reale, non per mostrare bravura.
Riuso o astrazione prematura?
Prima di creare un generic, chiediti:
- ho almeno due usi reali?
- il comportamento è davvero lo stesso?
- il nome del tipo generico comunica un concetto?
- sto migliorando la leggibilità o la sto rendendo più astratta?
Il riuso buono elimina duplicazione significativa. Il riuso cattivo nasconde il dominio.
Esercizio
Crea:
struct Pagina<Element>
struct Cache<Key: Hashable, Value>
struct Regola<Input>
struct RisultatoValidazione<Input>
Poi usa Regola<ContestoCoupon> per modellare:
- carrello minimo;
- primo ordine;
- presenza di una categoria nel carrello.
Infine scrivi una funzione:
func tutteSoddisfatte(_ regole: [Regola<ContestoCoupon>], da contesto: ContestoCoupon) -> Bool