W3docs

Astrazione in Java

Nasconde i dettagli implementativi dietro tipi astratti in Java usando classi astratte e interfacce.

L'astrazione è il quarto pilastro della OOP: descrivere cosa fa qualcosa senza impegnarsi in come lo fa. Si dichiarano le operazioni che un tipo supporta, si lascia l'implementazione alle classi concrete e si scrive il resto del programma contro il tipo astratto. Questo capitolo è la panoramica concettuale — i due meccanismi di Java per questo, abstract class e interface, hanno ciascuno un proprio capitolo dedicato.

Le due domande

Ogni dichiarazione di tipo in Java risponde a due domande:

  1. Cosa possono fare i chiamanti con i valori di questo tipo? (la sua API)
  2. Come viene implementata ciascuna di quelle operazioni? (il suo corpo)

Una classe concreta risponde a entrambe. Un tipo astratto risponde solo alla prima e lascia la seconda ai sottotipi:

public interface Shape {
  double area();         // what — every Shape has an area
}

public class Circle implements Shape {
  double r;
  public Circle(double r) { this.r = r; }
  public double area() { return Math.PI * r * r; }   // how
}
public class Square implements Shape {
  double side;
  public Square(double side) { this.side = side; }
  public double area() { return side * side; }
}

Shape dice "ogni forma ha un'area." Circle e Square dicono come calcolarne una. Il codice che accetta una Shape non si preoccupa di quale:

double sumAreas(List<Shape> shapes) {
  double sum = 0;
  for (Shape s : shapes) sum += s.area();
  return sum;
}

Questa funzione è chiusa sull'astrazione. Funziona per Circle e Square oggi; per Triangle domani; per Polygon tra sei mesi. Nessuno dei nuovi tipi richiede alcuna modifica a sumAreas.

I due meccanismi di Java

MeccanismoCosa fornisceQuando usarlo
abstract classUna classe parziale — alcuni metodi astratti, altri con corpo, più campi e costruttoriQuando i sottotipi condivideranno stato e codice infrastrutturale
interfaceUn contratto puro (o quasi puro) — metodi che le classi implementanti devono fornire; nessuno stato di istanzaQuando i sottotipi devono solo concordare su un insieme di operazioni e possono non avere nulla in comune

Una classe estende una sola classe astratta. Una classe può implementare molte interfacce. Questa asimmetria orienta molti design: se ci si trova a voler "ereditarietà multipla," le interfacce sono di solito la risposta.

Classi astratte — implementazione parziale

abstract su una classe significa "non puoi istanziarla direttamente — solo le sottoclassi." abstract su un metodo significa "nessun corpo qui; ogni sottoclasse concreta deve fornirne uno":

public abstract class Shape {
  public abstract double area();      // every Shape must define this

  // a concrete method, shared across all shapes
  public final String describe() {
    return getClass().getSimpleName() + " area=" + area();
  }
}

new Shape() è un errore di compilazione. new Circle() funziona. All'interno di describe, la chiamata area() viene dispacciata all'implementazione della sottoclasse effettiva — lo stesso meccanismo di polimorfismo di qualsiasi metodo sovrascritto.

Usa una classe astratta quando i sottotipi condividono davvero del codice. Se ti trovi a scrivere lo stesso helper in tre sottoclassi, è un segnale per spostarlo nel genitore.

Interfacce — il contratto

Un'interfaccia dichiara operazioni e lascia l'implementazione interamente a chi la implementa:

public interface Comparable<T> {
  int compareTo(T other);
}

public class Money implements Comparable<Money> {
  private final long cents;
  public int compareTo(Money other) {
    return Long.compare(this.cents, other.cents);
  }
}

Ora Money funziona ovunque sia attesa una ComparableCollections.sort(...), TreeMap, Arrays.sort(...), i tuoi stessi algoritmi generici. La libreria standard e il tuo codice concordano su Comparable come astrazione condivisa; nessuno dei due lati conosce l'altro.

La grande maggioranza delle interfacce standard di Java (List, Map, Iterable, Runnable, Function, Comparator, AutoCloseable) funziona così: un contratto piccolo e focalizzato in cui molte classi concrete si inseriscono.

L'astrazione come leva di design

La parte meccanica dell'astrazione — la parola chiave abstract, la dichiarazione interface — è piccola. La parte difficile è scegliere quali astrazioni definire. Tre pattern che compaiono ripetutamente:

  • Strategy. Definisci un'interfaccia per "l'algoritmo." Diverse implementazioni sostituiscono l'algoritmo senza modificare il codice che lo utilizza. Comparator è il classico esempio.
  • Template method. Una classe astratta implementa il flusso generale, con metodi astratti nei punti di variazione. Le sottoclassi riempiono i passaggi specifici. Il metodo service di HttpServlet è un esempio famoso.
  • Plugin / punto di estensione. Una libreria pubblica un'interfaccia; il codice utente la implementa; la libreria richiama al suo interno. Servlet API, driver JDBC, BeanPostProcessor di Spring.

In ogni caso, il guadagno è lo stesso: il codice che dipende dall'astrazione è chiuso rispetto ai cambiamenti nelle implementazioni, e aperto all'aggiunta di ulteriori implementazioni in seguito.

Incapsulamento vs astrazione

Questi due concetti sono cugini stretti e spesso si confondono.

  • L'incapsulamento nasconde l'implementazione di una singola classe specifica (campi privati, metodi controllati). È una preoccupazione interna alla classe.
  • L'astrazione nasconde quale classe si stia usando dietro un contratto condiviso. È una preoccupazione esterna alla classe.

Una classe con campi private e una API pubblica ordinata è incapsulata, ma non è ancora astratta — i chiamanti dipendono ancora da quella classe specifica. Sostituisci il tipo al confine dell'API con un'interfaccia e i chiamanti dipenderanno dal contratto. Ora puoi scambiare le implementazioni.

Per vederli lavorare insieme, guarda il capitolo sull'incapsulamento: l'incapsulamento blocca una singola classe, l'astrazione permette ai chiamanti di ignorare quale classe stiano usando.

Errori comuni

Alcune trappole colgono i nuovi arrivati all'astrazione:

  • Cercare di istanziare un tipo astratto. new Shape() è un errore di compilazione quando Shape è abstract o un'interfaccia. Si istanzia un sottotipo concreto (new Circle(2)) e lo si assegna al riferimento astratto.
  • Astrarre troppo presto. Un'interfaccia con esattamente un'implementazione, scritta "nel caso ne servisse un'altra in seguito," è di solito peso morto. Aggiungi l'astrazione quando compare la seconda implementazione, o quando hai genuinamente bisogno di disaccoppiare due moduli. L'astrazione prematura aggiunge indirezione senza guadagnare flessibilità.
  • Far trapelare il tipo concreto. Dichiarare un campo o parametro come ArrayList invece di List, o restituire HashMap invece di Map, lega i chiamanti a quella classe specifica e annulla l'astrazione. Preferisci il tipo più astratto che esprima ancora ciò di cui hai bisogno.
  • Confondere "nessun corpo" con "non fa nulla." Un metodo astratto non ha corpo perché le sottoclassi devono fornirne uno. Un metodo concreto con corpo vuoto è un metodo reale che non fa nulla — un contratto molto diverso.

Un esempio pratico

Esegui il programma seguente. Esercita entrambi i meccanismi: una classe Shape astratta con codice describe condiviso, e una pura interfaccia Greeter. L'output atteso è:

Circle area=12.566370614359172
Square area=9.0
total = 21.57

Dear Alice,
hey Alice!

Nota che totalArea e il ciclo sui greeter non nominano mai Circle, Square, FormalGreeter o CasualGreeter — parlano solo alle astrazioni Shape e Greeter.

java— editable, runs on the server

Cosa viene dopo

Il capitolo successivo tratta la meccanica concreta delle classi astratte — metodi astratti, cosa permettono di ereditare a una sottoclasse, quando preferirle alle interfacce.

Pratica

Pratica
Quale affermazione cattura meglio la differenza tra incapsulamento e astrazione?
Quale affermazione cattura meglio la differenza tra incapsulamento e astrazione?
Pratica
Quando dovresti preferire un'interfaccia rispetto a una classe astratta?
Quando dovresti preferire un'interfaccia rispetto a una classe astratta?
Was this page helpful?