cat swift/protocol-oriented-senza-dogmi.md
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:
- trova un punto dove un
enumè più adatto di un protocollo; - trova un punto dove un protocollo è davvero utile;
- crea un protocollo
CouponRepository; - crea due implementazioni: una in memoria è una finta per test;
- scrivi una frase che spiega perché il protocollo è giustificato.