Ereditarietà come meccanismo di astrazione: un esempio

Vediamo come usando l'ereditarietà si possono fattorizzare informazioni comuni a più classi.

Supponiamo di aver definito le classi Studente e Professore (ad esempio, sviluppando un'applicazione per la Segreteria della Facoltà):
 
 

public class Studente {
    static int prossimaMatricola = 1;
    String nome;
    String indirizzo;
    int matricola;
    String pianoDiStudio;

    Studente (String nome){
        this.nome = nome;
        this.matricola = prossimaMatricola++;
    }
    // <altri costruttori>

    String getNome(){ return nome; }
    String getIndirizzo(){ return indirizzo; }

    String getPianoDiStudio(){ return pianoDiStudio}
    void modificaPdS(String nuovoPdS){
        pianoDiStudio += "============\n" + nuovoPdS;
    }
}

public class Professore {
    String nome;
    String indirizzo;
    String ruolo;
    int stipendio;
    String corsiAffidati;

    Professore (String nome){
        this.nome = nome;
    }
    // <altri costruttori>

    String getNome(){ return nome; }
    String getIndirizzo(){ return indirizzo; }

    void aumentaStipedio(int aumento){
        stipendio += aumento;
    }
}

C'è ridondanza nel codice: variabili di istanza e metodi ripetuti nelle due classi (con lo stesso significato).




Superclasse per fattorizzare informazioni

Con l'ereditarietà possiamo fattorizzare le parti comuni in una nuova classe, da cui Studente e Professore ereditano. Per ovvi motivi, un nome "naturale" per questa classe è Persona.

Vediamo come usando l'ereditarietà si possono fattorizzare informazioni comuni a più classi.





Le classi Persona, Studentee Professore

public class Persona {
    String nome;
    String indirizzo;

    Persona (String nome){
        this.nome = nome;
    }
    // <altri costruttori>

    String getNome(){ return nome; }
    String getIndirizzo(){ return indirizzo; }
}

Nella nuova definizione delle due sottoclassi si noti la maggior compattezza e l'assenza di ridondanza:
 

public class Studente extends Persona{
    static int prossimaMatricola = 1;
    int matricola;
    String pianoDiStudio;

    Studente (String nome){
        super(nome);
        this.matricola = prossimaMatricola++;
    }
    // <altri costruttori>

    String getPianoDiStudio(){ return pianoDiStudio;}

    void modificaPdS(String nuovoPdS){
        pianoDiStudio += "============\n" + nuovoPdS;
    }
}



public class Professore extends Persona{
    String ruolo;
    int stipendio;
    String corsiAffidati;

    Professore (String nome){
        super(nome);

    }
    // <altri costruttori>

    void aumentaStipedio(int aumento){
        stipendio += aumento;
    }
}
 




Verso le Classi Astratte...

Le classi astratte consentono di usare questo meccanismo di "fattorizzazione" delle informazioni comuni, anche quando la superclasse risultante non sarebbe "ben definita".

Supponiamo di avere le classi Sfera  e Cubo:
 

public class Sfera{
    double raggio;
    double pesoSpecifico;
    Sfera(double raggio, double PS) {
        this.raggio = raggio;
        pesoSpecifico = PS;
    } 
    double volume(){ return 4/3 * Math.PI * Math.pow(raggio,3); }
    double superficie () { return 4 * Math.PI * raggio * raggio;}
    double peso(){ return pesoSpecifico * volume(); }
}


public class Cubo{
    double lato;
    double pesoSpecifico;
    Cubo(double lato, double PS){
        this.lato = lato;
        pesoSpecifico = PS;
    }
    double volume(){ return Math.pow(lato,3); }
    double superficie(){ return 6*lato*lato; }
    double peso(){ return pesoSpecifico * volume(); }
}
 




Sfera e Cubo come Solido

Anche qui troviamo ridondanza. Definiamo una classe Solido che fattorizza le informazioni comuni. Solido dovrebbe contenere:
  • la variabile pesoSpecifico;
  • il metodo peso(), identico nelle due classi;
  • i metodi volume() e superficie(), per due motivi:
    • ogni solido ha un "volume" e una "superficie"!
    • peso() invoca volume()...
Ma non c'è un modo default per calcolare superficie e volume di un solido generico!

SOLUZIONE: Definire  superficie() volume() come metodi astratti, aventi solo l'intestazione ma non l'implementazione. Di conseguenza Solido è una classe astratta.
 
 

abstract class Solido{
    double pesoSpecifico;
    double peso (){ return volume() * pesoSpecifico; }
    abstract double volume(); // metodo astratto
    abstract double superficie(); // metodo astratto
}



Classi Astratte

abstract class Solido{
    double pesoSpecifico;
    double peso (){ return volume() * pesoSpecifico; }
    abstract double volume(); // metodo astratto
    abstract double superficie(); // metodo astratto
}

Alcune cose importanti da ricordare:

  • Una classe astratta viene definita premettendo il modificatore abstract.
  • Non può essere istanziata, ma può avere costruttori.
  • Può contenere metodi astratti, cioè con modificatore abstract e senza corpo.
  • Metodi astratti possono essere dichiarati SOLO in classi astratte.
  • Se una classe concreta eredita da una classe astratta, deve fornire una implementazione per tutti i metodi astratti ereditati.
  • Classi astratte possono essere superclassi o sottoclassi di altre classi, sia astratte che non.
Vediamo la nuova definizione di Sfera e Cubo.
 
public class Sfera extends Solido
    double raggio;
    Sfera(double raggio, double PS) {
        this.raggio = raggio;
        pesoSpecifico = PS;
    } 
    double volume(){ return 4/3 * Math.PI * Math.pow(raggio,3); }
    double superficie () { return 4 * Math.PI * raggio * raggio;}
}


public class Cubo extends Solido{
    double lato;
    Cubo(double lato, double PS){
        this.lato = lato;
        pesoSpecifico = PS;
    }
    double volume(){ return Math.pow(lato,3); }
    double superficie(){ return 6*lato*lato; }
}
 




Esempi di classi astratte