CodeGym /Corsi /JAVA 25 SELF /Problemi e limiti dell'ereditarietà

Problemi e limiti dell'ereditarietà

JAVA 25 SELF
Livello 17 , Lezione 4
Disponibile

1. Limitazioni dell'ereditarietà in Java

Solo ereditarietà singola delle classi. In Java una classe può ereditare solo da un’altra classe. Questo si chiama ereditarietà singola. Per esempio, così — si può:

class Animal { }
class Dog extends Animal { }

Ma così — non si può:

class Animal { }
class Robot { }
// ERRORE! Java non supporta l'ereditarietà multipla delle classi
class RoboDog extends Animal, Robot { }

Se si prova a dichiarare una classe del genere, il compilatore dirà: "class RoboDog cannot extend multiple classes". Perché? Perché l’ereditarietà multipla porta ad ambiguità: se entrambi i genitori hanno un metodo con la stessa firma, quale usare? È il famoso «problema del diamante» (diamond problem).

Le interfacce in Java si possono implementare in numero illimitato, ma non le abbiamo ancora studiate. Ne parleremo più avanti.

I costruttori non vengono ereditati. Anche se la classe base ha un costruttore comodo, nella sottoclasse questo costruttore non apparirà automaticamente. Bisogna invocare esplicitamente il costruttore del genitore tramite super(...) nel costruttore della sottoclasse.

I membri privati non sono ereditati. Tutti i campi e i metodi (private) del genitore non sono accessibili nella sottoclasse. Esistono «all’interno» dell’oggetto, ma non è possibile accedervi direttamente.

2. Problemi di gerarchie fragili

Forte accoppiamento tra classi. Quando si crea una gerarchia di classi, le sottoclassi diventano strettamente accoppiate alla classe padre. Se si modifica la classe base, ciò può influenzare (o persino rompere) tutte le sue sottoclassi. Immaginate di avere la classe Animal, da cui ereditano Dog, Cat, Bird e un’altra decina. Se modificate la struttura di Animal (per esempio, aggiungete un nuovo parametro obbligatorio al costruttore), dovrete passare in rassegna tutte le classi figlie e aggiornare il loro codice. Questo è particolarmente doloroso nei progetti grandi.

Il problema dell’ereditarietà «che si rompe». A volte una sottoclasse può cambiare inavvertitamente un comportamento su cui conta la classe base. Per esempio, la classe padre invoca un proprio metodo all’interno di un altro metodo, e la sottoclasse sovrascrive quel metodo cambiandone la logica. Di conseguenza la classe padre inizia a comportarsi diversamente da quanto previsto.

class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
    void sleep() {
        System.out.println("Animal is going to sleep...");
        makeSound(); // Il genitore chiama il proprio metodo
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.sleep();
    }
}

Cosa stamperà il programma?

Animal is going to sleep...
Woof!

La classe padre presumeva che makeSound() fosse la sua implementazione, ma in realtà verrà chiamata la versione della sottoclasse! Questo può portare a bug inattesi se la sottoclasse sovrascrive il metodo con una logica diversa.

3. Problema della classe base fragile (fragile base class problem)

È un problema reale nei progetti di grandi dimensioni. Se modificate la classe base (per esempio, aggiungete un campo, cambiate l’implementazione di un metodo), rischiate di rompere il comportamento di tutte le sottoclassi. Talvolta ciò non si manifesta subito e la ricerca di un errore del genere può richiedere ore o addirittura giorni.

Illustrazione: supponiamo di avere una classe Shape con il metodo draw(). Decidete di aggiungere a Shape un nuovo metodo drawShadow(), che chiama draw(). Ma una delle sottoclassi (Circle) sovrascrive draw(), e ora, quando si invoca drawShadow() su Circle, il comportamento potrebbe risultare inatteso.

4. Forte accoppiamento e difficoltà di refactoring

Quando le classi sono collegate tramite ereditarietà, la modifica di una classe può interessare un’intera catena di dipendenze. Ciò rende il codice meno flessibile, complica il refactoring e l’estensione. A volte bisogna riscrivere intere gerarchie per aggiungere una nuova funzionalità.

Esempio reale

class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class Bicycle extends Vehicle { /* ... */ }
class Bus extends Vehicle { /* ... */ }

Improvvisamente arriva una richiesta: «Aggiungiamo un monopattino elettrico!». Ma il monopattino elettrico è sia un mezzo di trasporto sia un gadget. Che fare? Se iniziate ad allargare la gerarchia per far rientrare tutte le nuove entità, diventerà rapidamente ingestibile.

5. Problema del riuso del codice senza relazione logica

Molto spesso i programmatori (principianti e non solo) usano l’ereditarietà per riutilizzare codice, anche se tra le classi non c’è una relazione «è un» (is-a). Questo porta a un’architettura errata.

Esempio di ereditarietà sbagliata

class DatabaseUtils {
    void connect() { /* ... */ }
    void disconnect() { /* ... */ }
}

class User extends DatabaseUtils { // L'utente non "è" un'utilità del database!
    String name;
}

È più corretto usare la composizione: rendere DatabaseUtils una classe separata e chiamarne i metodi dove serve, invece di ereditare da essa.

6. Alternative all'ereditarietà

Composizione (has-a)

Se un oggetto «contiene» un altro oggetto, usate la composizione. Ad esempio, la classe Car può avere un campo Engine:

class Engine { /* ... */ }

class Car {
    private Engine engine;
    // ...
}

Delegazione

Invece di estendere una classe, delegate l’esecuzione del compito a un altro oggetto. Questo preserva la flessibilità e riduce l’accoppiamento tra componenti.

Interfacce

In Java una classe può implementare un numero qualsiasi di interfacce. Questo consente di combinare in modo flessibile i comportamenti senza una gerarchia rigida. Torneremo sulle interfacce più avanti.

Quando conviene usare l'ereditarietà?

Usate l’ereditarietà solo se tra le classi c’è una chiara relazione «è un» (is-a):

  • Il gatto è un animale (Cat extends Animal)
  • Il cerchio è una figura (Circle extends Shape)
  • L’amministratore è un utente (Admin extends User)

Non usate l’ereditarietà solo per riutilizzare codice — per questo ci sono composizione e delegazione.

7. Alcuni esempi pratici

Esempio: gerarchia eccessivamente complessa

class Animal { }
class Mammal extends Animal { }
class Cat extends Mammal { }
class PersianCat extends Cat { }
class SuperPersianCat extends PersianCat { }

Se la vostra gerarchia scende oltre tre livelli — riflettete: non è forse il caso di fermarsi? Gerarchie troppo profonde complicano la comprensione e la manutenzione del codice.

Esempio: gerarchia piatta

class Animal { }
class Cat extends Animal { }
class Dog extends Animal { }
class Bird extends Animal { }
class Fish extends Animal { }
class Spider extends Animal { }
class Platypus extends Animal { }
class Dragon extends Animal { }

Se avete decine di sottoclassi, ciascuna differente solo per un metodo, forse conviene usare interfacce o composizione.

8. Errori tipici nell'uso dell'ereditarietà

Errore n. 1: ereditarietà senza relazione «è un».
Se la sottoclasse in realtà non è una variante del genitore, l’architettura diventa innaturale e rapidamente fuori controllo. Ad esempio, la classe User non deve ereditare da DatabaseUtils, anche se può sembrare «comodo».

Errore n. 2: override dei metodi cambiandone il contratto.
Se sovrascrivete un metodo e ne cambiate la logica al punto da non rispettare più le aspettative del genitore, questo porterà a errori inattesi. Per esempio, se la classe base si aspetta che il metodo draw() disegni una figura, ma nella sottoclasse all’improvviso inizia a eseguire effetti collaterali pericolosi — è una catastrofe.

Errore n. 3: gerarchie troppo profonde o troppo piatte.
Una gerarchia troppo profonda rende difficile capire il codice; una troppo piatta porta a duplicazioni.

Errore n. 4: tentare di aggirare i limiti del linguaggio.
Si cerca di implementare l’ereditarietà multipla con «stampelle» (copia-incolla, superclassi «di utilità»), il che porta al caos.

Errore n. 5: uso cieco dell’ereditarietà per riutilizzare codice.
Spesso porta a legami inattesi tra classi, complica i test e la manutenzione. Usate composizione e delegazione.

1
Sondaggio/quiz
Ereditarietà e gerarchia, livello 17, lezione 4
Non disponibile
Ereditarietà e gerarchia
Ereditarietà e gerarchia
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION