CodeGym /Corsi /JAVA 25 SELF /Implementazione di astrazioni e gerarchie

Implementazione di astrazioni e gerarchie

JAVA 25 SELF
Livello 19, Lezione 2
Disponibile

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
abstract
abstract
-
Circle implementato implementato
radius
Rectangle implementato implementato
width, height

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à.

1
Compito
JAVA 25 SELF, livello 19, lezione 2
Bloccato
Simulatore del traffico stradale: la macchina si muove 🛣️
Simulatore del traffico stradale: la macchina si muove 🛣️
1
Compito
JAVA 25 SELF, livello 19, lezione 2
Bloccato
Biblioteca delle figure geometriche 🔵⬛
Biblioteca delle figure geometriche 🔵⬛
1
Compito
JAVA 25 SELF, livello 19, lezione 2
Bloccato
Sistema di gestione del personale di base 🧑‍💼
Sistema di gestione del personale di base 🧑‍💼
1
Compito
JAVA 25 SELF, livello 19, lezione 2
Bloccato
Sistema di pagamento flessibile per il negozio 💰
Sistema di pagamento flessibile per il negozio 💰
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION