cat swift/generics-riuso-significato.md

Lezione 1045 min

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