1. Costruire una gerarchia: dall’astrazione ai dettagli
L’implementazione di astrazioni e gerarchie è un modo per strutturare il codice dalle regole generali ai dettagli concreti. Prima descriviamo cosa devono saper fare tutti gli oggetti (astrazione), poi precisiamo come lo fa ciascuna classe concreta.
In programmazione, come nella vita, tutto parte dalle domande. Per esempio: «Che cosa hanno in comune un cerchio e un rettangolo?» Risposta: entrambi sono figure. E cosa hanno in comune le figure? Di solito hanno un’area e possono essere disegnate.
In Java questo si esprime tramite una classe abstract:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
Qui stiamo dicendo:
- Qualsiasi figura deve saper calcolare la propria area (area()).
- Qualsiasi figura deve sapersi disegnare (draw()).
- Come lo faccia nello specifico — non è affar nostro (per ora).
Ora creiamo figure concrete:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("Disegniamo un cerchio con raggio " + radius);
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("Disegniamo un rettangolo " + width + "x" + height);
}
}
Cosa abbiamo fatto?
- Abbiamo estratto il comune in una classe astratta.
- Abbiamo dettagliato il comportamento nelle sottoclassi.
Schema:
. Shape
/ \
Circle Rectangle
Tabella: che cosa è implementato dove
| Classe | area() | draw() | Campi propri |
|---|---|---|---|
| Shape | |
|
- |
| Circle | implementato | implementato | |
| Rectangle | implementato | implementato | |
2. Perché è comodo? (e perché funziona)
Un’unica interfaccia per lavorare con oggetti diversi
Supponiamo di avere una collezione di figure:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
Potete iterarle allo stesso modo, senza pensare al tipo:
for (Shape shape : shapes) {
shape.draw();
System.out.println("Area: " + shape.area());
}
Lasciate che sia la JVM a capire da sola chi è un cerchio e chi è un rettangolo! Questo è il polimorfismo (ne abbiamo già parlato e ne parleremo ancora più nel dettaglio — nel prossimo blocco).
Facilità di estensione
Volete aggiungere un triangolo? Basta scrivere (tipo nuovo — il vecchio codice non si tocca): Triangle extends Shape.
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("Disegniamo un triangolo: base " + base + ", altezza " + height);
}
}
Non serve modificare il resto del codice (per esempio, il ciclo che scorre l’elenco delle figure).
Evitare la duplicazione del codice
Se tutte le figure acquisiscono una proprietà comune (per esempio, il colore), è comodo estrarla nella classe astratta:
public abstract class Shape {
private String color = "nero";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
Ora qualsiasi sottoclasse — che sia un cerchio o un triangolo — otterrà il colore «per eredità».
3. Pratica: sviluppiamo un mini editor grafico
Mettiamo tutto insieme. Immaginate di realizzare un semplice editor grafico.
Classe astratta Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
Figure concrete
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Disegniamo una linea di lunghezza " + length + " di colore " + getColor());
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Nuova lunghezza della linea: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("Disegniamo un'ellisse con assi " + a + " e " + b + " di colore " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Nuovi assi dell'ellisse: " + a + ", " + b);
}
}
Volete aggiungere un nuovo strumento, ad esempio Polygon? Create una nuova classe — tutto l’editor funziona tramite l’astrazione Figure.
Uso nel codice
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
Output:
Disegniamo una linea di lunghezza 10.0 di colore red
Nuova lunghezza della linea: 15.0
Disegniamo un'ellisse con assi 5.0 e 3.0 di colore red
Nuovi assi dell'ellisse: 7.5, 4.5
Visualizzazione della gerarchia
. Figure
/ \
Line Ellipse
4. Come evitare la duplicazione: campi e metodi comuni
A volte tutte le sottoclassi hanno non solo metodi comuni, ma anche campi comuni (per esempio, le coordinate del centro). La classe astratta è il posto ideale per questo:
public abstract class Figure {
private double x, y; // coordinate del centro
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("Figura spostata al punto (" + x + ", " + y + ")");
}
public abstract void draw();
}
Ora qualsiasi Line o Ellipse può essere spostata senza dover implementare di nuovo questo metodo.
5. Un altro esempio: sistemi di pagamento
L’astrazione non riguarda solo le figure! Immaginate di scrivere un sistema di elaborazione dei pagamenti.
Classe astratta Payment
public abstract class Payment {
public abstract void process();
}
Implementazioni concrete
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("Elaborazione del pagamento con carta di credito");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("Elaborazione del pagamento tramite PayPal");
}
}
Utilizzo
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
Output:
Elaborazione del pagamento con carta di credito
Elaborazione del pagamento tramite PayPal
6. Vantaggi di questo approccio
- Un’unica interfaccia: è possibile lavorare con oggetti diversi allo stesso modo.
- Estendibilità: aggiungere nuovi tipi di oggetti non richiede di riscrivere il vecchio codice.
- Minima duplicazione: il comune è estratto nella classe astratta di base.
- Flessibilità: si possono usare collezioni di tipi astratti senza preoccuparsi dei dettagli.
7. Esempio dalla vita reale: trasporti
L’astrazione non si incontra solo nei manuali. Per esempio, se progettate un sistema di gestione dei trasporti:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
I tipi concreti di trasporto implementano i dettagli:
public class Car extends Transport {
@Override
public void move() {
System.out.println("L'auto percorre la strada");
}
@Override
public void fuelUp() {
System.out.println("Facciamo rifornimento di benzina");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("La bicicletta pedala");
}
@Override
public void fuelUp() {
System.out.println("La bicicletta non ha bisogno di carburante, solo di panini per il ciclista!");
}
}
8. Schema utile: come costruire una gerarchia di astrazioni
[Classe astratta]
|
[Sottoclasse concreta]
|
[Sottoclasse ancora più concreta] (se necessario)
- Tutto ciò che è comune — in alto!
- Tutto ciò che è specifico — in basso!
9. Errori tipici nell’implementazione di astrazioni e gerarchie
Errore n. 1: duplicazione di codice nelle sottoclassi.
Se notate che in ogni sottoclasse scrivete gli stessi campi o metodi, è un segnale che dovreste estrarli nella classe astratta. Non temete di rendere l’astrazione «più ampia» se questo riduce la duplicazione.
Errore n. 2: violare il principio «dal generale al particolare».
A volte i principianti iniziano a costruire la gerarchia «dai dettagli», dimenticando il generale. Di conseguenza compaiono classi strane come RedCircleWithShadow, che si integrano male nella struttura complessiva. Identificate sempre prima l’astrazione, poi i dettagli.
Errore n. 3: gerarchie troppo profonde.
Se la vostra catena di ereditarietà è più lunga di 3–4 livelli, chiedetevi se non sia il caso di usare composizione o interfacce al posto dell’ereditarietà.
Errore n. 4: imporre l’implementazione di metodi non pertinenti.
Se una classe astratta contiene troppi metodi astratti non pertinenti per alcune sottoclassi, forse è il caso di rivedere la struttura. Per esempio, non tutti i mezzi di trasporto hanno bisogno del metodo fuelUp() (per la bicicletta non serve).
Errore n. 5: confusione tra classe astratta e interfaccia.
Classe astratta — quando esiste stato comune e/o un’implementazione parziale. Interfaccia — quando serve solo «promettere» la presenza di metodi, senza conservare dati o implementare comportamento. Non mescolate questi approcci senza necessità.
GO TO FULL VERSION