cat swift/access-control-confini-modulo.md

Lezione 12 40 min

Access control è confini del modulo

public, internal, private e private(set): proteggere invarianti e rendere chiaro cosa fa parte dell'API.

Modellare bene il dominio non significa solo creare tipi giusti. Significa anche proteggere i loro confini.

Swift offre l’access control per decidere chi può leggere, costruire o modificare un tipo.

I livelli più comuni sono:

  • private: visibile solo nello scope corrente;
  • fileprivate: visibile nello stesso file;
  • internal: visibile nel modulo, default;
  • public: visibile fuori dal modulo;
  • open: come public, ma pensato per ereditarietà e override fuori modulo.

In questo corso useremo soprattutto private, internal, public e private(set).

Perché i confini contano

Nel laboratorio precedente avevamo:

struct Prezzo {
  let centesimi: Int

  init?(_ centesimi: Int) {
    guard centesimi >= 0 else {
      return nil
    }

    self.centesimi = centesimi
  }
}

Questo protegge da prezzi negativi, ma centesimi è leggibile da tutti. Va bene. Il problema nasce quando una proprietà è modificabile.

struct Coupon {
  var codice: String
  var scontoPercentuale: Int
}

Chiunque può scrivere:

coupon.scontoPercentuale = 999

Il tipo non protegge il suo dominio.

private(set)

private(set) permette di leggere una proprietà dall’esterno, ma modificarla solo dall’interno del tipo.

struct Carrello {
  private(set) var righe: [RigaCarrello] = []

  mutating func aggiungi(_ riga: RigaCarrello) {
    righe.append(riga)
  }
}

Chi usa Carrello può leggere righe, ma non può sostituirle liberamente.

Questo consente al tipo di controllare le modifiche.

Initializer come porta d’ingresso

Gli initializer sono confini fondamentali.

struct CodiceCoupon: Equatable {
  let valore: String

  init?(_ valore: String) {
    let normalizzato = valore.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()

    guard normalizzato.isEmpty == false else {
      return nil
    }

    self.valore = normalizzato
  }
}

Ora un codice vuoto non può esistere come CodiceCoupon.

Ma se valore è pubblico e modificabile, perdi tutto:

struct CodiceCoupon {
  var valore: String
}

Il tipo deve proteggere il suo stato anche dopo la creazione.

public API e dettagli interni

Quando creeremo un package Swift, dovremo distinguere:

  • cosa chi usa la libreria può vedere;
  • cosa resta dettaglio interno.

Esempio:

public struct CouponEngine {
  public init() {}

  public func applica(_ coupon: Coupon, a carrello: Carrello) throws -> Carrello {
    ...
  }
}

Se CouponEngine deve essere usato fuori dal modulo, deve essere public.

Ma una funzione di supporto può restare private:

private func calcolaRiduzione(...) -> Int {
  ...
}

Non tutto merita di diventare API.

internal come default

In Swift, se non scrivi nulla, il livello è internal: visibile dentro lo stesso modulo.

Per una app piccola va bene. Per una libreria, invece, devi essere intenzionale: tutto ciò che è public diventa contratto.

Cambiare un dettaglio interno è facile. Cambiare un’API pubblica rompe chi la usa.

Nascondere invarianti

Supponiamo di voler evitare il force unwrap in Prezzo.

Possiamo creare un initializer privato sicuro da usare internamente:

struct Prezzo: Equatable {
  let centesimi: Int

  init?(_ centesimi: Int) {
    guard centesimi >= 0 else {
      return nil
    }

    self.centesimi = centesimi
  }

  private init(validato centesimi: Int) {
    self.centesimi = centesimi
  }

  func sommando(_ altro: Prezzo) -> Prezzo {
    Prezzo(validato: centesimi + altro.centesimi)
  }
}

Fuori dal tipo, chi crea un prezzo deve passare dalla validazione. Dentro il tipo, dove conosciamo l’invariante, possiamo costruire direttamente.

Estendere senza esporre troppo

Un errore comune è rendere pubbliche proprietà solo per usarle nei test o in altri file. Prima chiediti se stai rompendo l’incapsulamento.

Alternative:

  • aggiungere metodi espliciti;
  • usare private(set);
  • spostare logica nello stesso tipo;
  • creare inizializzatori di test in target separati;
  • verificare comportamenti invece di dettagli.

Checklist

Quando progetti un tipo, chiediti:

  • quali proprietà devono essere lette dall’esterno?
  • quali possono essere modificate?
  • chi deve poter costruire valori validi?
  • cosa è dettaglio interno?
  • cosa diventerà API pubblica?
  • sto esponendo stato per comodità o per necessità?

Esercizio

Rivedi i tipi del motore coupon:

  • CodiceCoupon;
  • Prezzo;
  • ScontoPercentuale;
  • Carrello;
  • CouponEngine.

Per ciascuno decidi:

  • quali proprietà sono let;
  • quali sono private(set);
  • quali initializer sono pubblici;
  • quali helper restano private.

Poi scrivi un esempio di stato invalido che l’access control ora impedisce.