W3docs

Incapsulamento in Java

Raggruppa dati e metodi nelle classi Java e nascondi i dettagli implementativi usando campi privati e metodi di accesso pubblici.

L'incapsulamento è il primo dei quattro pilastri della OOP, e il più semplice da mettere in pratica: mantieni i dati di una classe privati ed esponi il comportamento tramite metodi. La classe diventa responsabile del proprio stato — nessun codice esterno può portarla in uno stato non valido — e il resto del programma interagisce con essa attraverso un'interfaccia che controlli tu.

Il meccanismo è: campi private più metodi public. La disciplina è: "non esporre uno stato che non puoi controllare."

Il pattern

Una classe non incapsulata è solo un contenitore di campi pubblici:

public class Account {
  public int balance;
}

Account a = new Account();
a.balance = 100;
a.balance = -50;   // nothing stops this

La versione incapsulata nasconde il campo ed espone operazioni deliberate:

public class Account {
  private int balance;

  public int  balance()              { return balance; }
  public void deposit(int amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    balance += amount;
  }
  public boolean withdraw(int amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    if (amount > balance) return false;
    balance -= amount;
    return true;
  }
}

Ora non c'è modo per il codice esterno di impostare balance a un numero negativo, sovrascriverlo senza passare per la validazione, o leggerlo senza passare per balance().

Cosa si guadagna

Invarianti di cui ci si può fidare. La classe Account impone che "il saldo sia ≥ 0 dopo ogni operazione pubblica." Poiché nulla all'esterno può toccare balance, la regola è applicabile da un unico file, non dall'intera codebase.

Libertà di cambiare. Con balance privato, si può successivamente cambiare il suo tipo da int a long, cambiare l'unità da dollari a centesimi, memorizzarlo in un BigDecimal, o spostarlo in un database — senza toccare nemmeno un chiamante. Con un campo pubblico, il tipo e lo storage sono incorporati in ogni sito di chiamata.

Un'API più piccola e chiara. I chiamanti vedono solo deposit, withdraw, balance — non la dozzina di metodi helper privati che li fanno funzionare. La classe pubblicizza cosa fa, non come.

Ragionamento localizzato. Quando qualcosa va storto con balance, il bug si trova in uno dei tre metodi, non in uno qualsiasi dei mille posti che potrebbero altrimenti scrivere nel campo.

Incapsulamento ≠ getter e setter per tutto

Un anti-pattern comune è generare meccanicamente un getter e un setter per ogni campo:

public class Account {
  private int balance;
  public int  getBalance()           { return balance; }
  public void setBalance(int v)      { this.balance = v; }    // same as public field, with extra steps
}

Questo è tecnicamente "incapsulato" nel senso del libro di testo, ma non raggiunge nessuno dei reali benefici dell'incapsulamento — chiunque può ancora mettere l'oggetto in qualsiasi stato desideri. Il vero incapsulamento esprime operazioni, non accesso grezzo ai campi:

  • deposit(amount) invece di setBalance(balance + amount)
  • withdraw(amount) che restituisce successo/fallimento invece di setBalance(balance - amount)
  • balance() (un accessor in sola lettura) senza il corrispondente setBalance

Il prossimo capitolo su getter e setter illustra le convenzioni per quando ciascuno è appropriato.

Copie difensive

Se il tipo di un campo è mutabile (un array, una lista, un Date), restituirlo direttamente fa trapelare il controllo:

public class Order {
  private final List<String> items = new ArrayList<>();
  public List<String> items() { return items; }      // leak!
}

Order o = new Order();
o.items().add("apple");                              // outside code mutated the order

La soluzione è restituire una vista non modificabile o una copia difensiva:

public List<String> items() {
  return List.copyOf(items);          // immutable snapshot
}

La stessa attenzione si applica ai setter che accettano valori mutabili — copiali in entrata:

public Order(List<String> items) {
  this.items = new ArrayList<>(items);
}

Il capitolo sulle classi immutabili approfondisce l'argomento.

Scegliere il giusto livello di accesso

private e public sono i due che si usano più spesso, ma Java ha quattro livelli, e quelli intermedi contano per l'incapsulamento:

  • private — visibile solo all'interno della stessa classe. Il default per i campi e i metodi helper.
  • package-private (nessuna parola chiave) — visibile alle altre classi nello stesso package. Utile quando alcune classi che collaborano formano un'unica unità e devono vedere le rispettive implementazioni interne, ma il mondo esterno non dovrebbe.
  • protected — package-private più visibile alle sottoclassi. Riservalo ai membri che una sottoclasse deve davvero sovrascrivere o su cui costruire.
  • public — visibile ovunque. Questa è la tua API pubblicata; una volta che il codice esterno dipende da essa, cambiarla rompe i chiamanti.

La regola pratica è il principio del minimo privilegio: assegna a ogni membro il livello di accesso più ristretto che consente ancora al codice di funzionare, e allargalo solo quando emerge un'esigenza concreta. Un campo o metodo public è una promessa che devi mantenere. Vedi il capitolo sui modificatori di accesso per la tabella completa.

Incapsulamento nel design

Oltre una certa dimensione, l'incapsulamento diventa uno strumento di design, non solo una regola di codifica. Ogni classe traccia un confine attorno a una porzione di stato e alle operazioni su di essa; il resto del sistema vi comunica attraverso quel confine. La maggior parte dei pattern architetturali — a strati, esagonale, MVC, Clean — sono argomenti su dove tracciare i confini.

Linee guida pratiche:

  • I campi sono privati finché non si dimostra il contrario. Inizia da lì; allarga solo con una motivazione.
  • Esponi verbi, non sostantivi. cancel(), pay(), ship() sono meglio di setStatus(...).
  • Non restituire stati interni mutabili grezzi. Incapsula, copia o usa una vista immutabile.
  • Valida all'ingresso. Rifiuta lo stato impossibile al confine così il codice interno può assumere che sia valido.

Un esempio pratico

java— editable, runs on the server

Cosa viene dopo

La parte meccanica dell'incapsulamento — i campi private con metodi public che li leggono o li scrivono — ha le proprie convenzioni che vale la pena conoscere. Continua con getter e setter.

Pratica

Pratica
Perché generare meccanicamente un getter pubblico e un setter pubblico per ogni campo privato vanifica lo scopo dell'incapsulamento?
Perché generare meccanicamente un getter pubblico e un setter pubblico per ogni campo privato vanifica lo scopo dell'incapsulamento?
Was this page helpful?