cat swift/composizione-vs-ereditarieta.md

Lezione 17 40 min

Composizione vs ereditarietà

In Swift spesso è meglio comporre piccoli tipi e comportamenti invece di costruire gerarchie rigide.

L’ereditarietà è uno strumento potente, ma in Swift non è il punto di partenza. Molto codice idiomatico preferisce la composizione: costruire tipi più grandi combinando tipi piccoli.

La composizione rende le dipendenze più visibili, riduce accoppiamento e funziona molto bene con struct, enum e protocol.

Il problema delle gerarchie

Immagina di modellare promozioni:

class Promozione {
  func sconto(per carrello: Carrello) -> Prezzo {
    .zero
  }
}

class PromozionePercentuale: Promozione {
  override func sconto(per carrello: Carrello) -> Prezzo {
    ...
  }
}

class PromozioneCategoria: Promozione {
  override func sconto(per carrello: Carrello) -> Prezzo {
    ...
  }
}

Funziona, ma porta con se alcuni problemi:

  • identità di riferimento anche se non serve;
  • stato potenzialmente condiviso;
  • override difficili da seguire;
  • gerarchie rigide quando le regole si combinano;
  • test più macchinosi.

Composizione con valori

Un’alternativa:

struct Promozione {
  let nome: String
  let regole: [RegolaCoupon]
  let sconto: Sconto
}

Ora una promozione non è una sottoclasse. È una combinazione di:

  • nome;
  • regole;
  • sconto.

Puoi creare molte promozioni senza creare nuove classi.

Comporre comportamenti

Con una struct generica:

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

Puoi comporre regole:

let carrelloMinimo = Regola<ContestoCoupon>(
  descrizione: "Carrello minimo 50 euro",
  valida: { $0.carrello.totaleInCent >= 5000 }
)

let primoOrdine = Regola<ContestoCoupon>(
  descrizione: "Solo primo ordine",
  valida: { $0.utente.haGiaOrdinato == false }
)

Il comportamento è dato dalla composizione di valori, non dall’ereditarietà.

Composizione con protocol

Puoi comporre anche dipendenze:

struct CouponService {
  let repository: CouponRepository
  let engine: CouponEngine
}

CouponService non eredita da niente. Riceve ciò che gli serve.

Questo rende chiaro il suo mondo:

  • da dove arrivano i coupon;
  • chi li valida;
  • chi applica lo sconto.

Quando l’ereditarietà ha senso

L’ereditarietà non è vietata.

Ha senso quando:

  • un framework la richiede;
  • stai lavorando con classi UIKit/AppKit;
  • devi specializzare comportamento in una gerarchia già esistente;
  • hai un vero rapporto “è un tipo di” e non solo “usa”.

Ma nel dominio puro, spesso puoi evitarla.

Rapporto is-a vs has-a

Ereditarietà:

class CouponPercentuale: Coupon {}

Sta dicendo: un coupon percentuale è un coupon.

Composizione:

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

Sta dicendo: un coupon ha uno sconto.

Spesso il secondo modello è più flessibile.

Composizione e test

Un tipo composto è più facile da testare.

let service = CouponService(
  repository: RepositoryInMemoria(coupon: [...]),
  engine: CouponEngine()
)

Non devi creare sottoclassi finte. Puoi passare implementazioni piccole e controllate.

Odori di ereditarietà

Fai attenzione quando:

  • una sottoclasse disabilita metodi della superclasse;
  • devi controllare il tipo concreto con is o as?;
  • la gerarchia cresce in profondità;
  • la superclasse contiene stato usato solo da alcune sottoclassi;
  • i test richiedono molta preparazione.

Questi segnali indicano che forse la composizione sarebbe più chiara.

Checklist

Preferisci composizione quando:

  • stai modellando dati di dominio;
  • vuoi combinare regole;
  • vuoi testare facilmente;
  • il comportamento cambia per configurazione;
  • puoi usare enum o struct invece di sottoclassi.

Valuta ereditarietà quando:

  • il framework lo richiede;
  • devi estendere una classe esistente;
  • il rapporto gerarchico è davvero stabile.

Esercizio

Parti da questa idea:

class Promozione {}
class PromozionePercentuale: Promozione {}
class PromozioneCategoria: Promozione {}
class PromozionePrimoOrdine: Promozione {}

Riscrivila usando:

  • struct Promozione;
  • enum Sconto;
  • [RegolaCoupon];
  • CouponEngine.

Poi aggiungi una nuova promozione combinata: “10% sui libri solo al primo ordine con carrello minimo 50 euro”. Dovresti riuscirci senza creare nuove classi.