cat swift/protocol-oriented-senza-dogmi.md

Lezione 16 45 min

Protocol-oriented programming senza dogmi

I protocol sono potenti quando descrivono capacità reali; diventano rumore quando sostituiscono il giudizio.

Swift ha reso famosa l’espressione protocol-oriented programming. Il rischio è trasformarla in una religione: creare protocolli per ogni tipo, ogni servizio, ogni modello, ogni funzione.

Il risultato è spesso un codice più astratto, ma non più chiaro.

Un protocollo dovrebbe nascere da una domanda precisa:

Quale capacità voglio esprimere, e quali tipi diversi possono offrirla?

Protocol come contratto

protocol Scontabile {
  func scontoApplicabile(a carrello: Carrello) -> Prezzo
}

Questo protocollo ha senso se esistono più fonti di sconto:

struct CouponPercentuale: Scontabile {
  let percentuale: Int

  func scontoApplicabile(a carrello: Carrello) -> Prezzo {
    carrello.totale.percentuale(percentuale)
  }
}

struct PromozioneCategoria: Scontabile {
  let categoria: CategoriaProdotto
  let importo: Prezzo

  func scontoApplicabile(a carrello: Carrello) -> Prezzo {
    carrello.contieneCategoria(categoria) ? importo : .zero
  }
}

Qui il protocollo non è decorazione. Permette di trattare tipi diversi attraverso una capacità comune.

Protocol inutile

Questo invece è sospetto:

protocol CouponProtocol {
  var codice: String { get }
  var valore: Int { get }
}

struct Coupon: CouponProtocol {
  let codice: String
  let valore: Int
}

Se hai un solo tipo e nessuna esigenza di sostituzione, il protocollo non aggiunge informazione.

Versione migliore:

struct Coupon {
  let codice: CodiceCoupon
  let sconto: Sconto
}

Protocol per separare dipendenze

I protocolli sono utili anche quando vuoi separare il dominio da una dipendenza esterna.

protocol CouponRepository {
  func couponDisponibili(per utente: Utente) async throws -> [Coupon]
}

Il dominio può dipendere dal contratto, non da un client HTTP concreto.

struct CouponService {
  let repository: CouponRepository

  func miglioriCoupon(per utente: Utente) async throws -> [Coupon] {
    try await repository.couponDisponibili(per: utente)
  }
}

Questo sarà importante nei moduli su networking, testabilità è architettura.

Protocol con default implementation

Le extension sui protocolli possono fornire implementazioni di default.

protocol IdentificabileConCodice {
  var codice: String { get }
}

extension IdentificabileConCodice {
  var codiceNormalizzato: String {
    codice.uppercased()
  }
}

Comodo, ma attenzione: troppe default implementation possono nascondere comportamento e rendere difficile capire quale codice viene eseguito.

Protocol e associatedtype

Un protocollo può avere un tipo associato:

protocol Validatore {
  associatedtype Input

  func valida(_ input: Input) -> Bool
}

Questo rende il protocollo generico, ma anche più complesso da usare.

struct ValidatoreCarrello: Validatore {
  func valida(_ input: Carrello) -> Bool {
    input.righe.isEmpty == false
  }
}

Usa associatedtype quando il tipo di input è parte del contratto. Evitalo quando una semplice funzione o struct generica basta.

Existential: any Protocol

Swift moderno rende esplicito l’uso di valori protocollo con any.

let validatori: [any ValidatoreCoupon] = [...]

Quando scrivi any, stai dicendo: qui posso avere valori concreti diversi, conosciuti solo attraverso il protocollo.

Questa flessibilità ha un costo concettuale. Non usarla se non ti serve eterogeneita reale.

Spesso puoi preferire generics:

func esegui<V: ValidatoreCoupon>(_ validatore: V, su coupon: Coupon) -> Bool {
  validatore.valida(coupon)
}

Oppure una struct con closure:

struct Regola<Input> {
  let valida: (Input) -> Bool
}

Protocol o enum?

Molti problemi che sembrano richiedere protocolli sono in realtà stati chiusi.

Se conosci tutte le alternative, un enum può essere migliore:

enum Sconto {
  case percentuale(Int)
  case importoFisso(Prezzo)
  case spedizioneGratuita
}

Se invece vuoi permettere nuove implementazioni senza modificare il tipo, un protocollo può avere senso.

Domanda chiave:

Il mondo è chiuso o aperto?

  • Chiuso: enum.
  • Aperto: protocol.

Checklist

Prima di creare un protocollo, chiediti:

  • ho più tipi concreti che condividono una capacità?
  • voglio sostituire una dipendenza nei test?
  • voglio un mondo aperto a nuove implementazioni?
  • il nome del protocollo descrive un comportamento?
  • sto creando astrazione per bisogno o per abitudine?

Esercizio

Nel motore coupon:

  1. trova un punto dove un enum è più adatto di un protocollo;
  2. trova un punto dove un protocollo è davvero utile;
  3. crea un protocollo CouponRepository;
  4. crea due implementazioni: una in memoria è una finta per test;
  5. scrivi una frase che spiega perché il protocollo è giustificato.