Sommario
< Home
Stampa

Ereditarietà

Ora che abbiamo visto una panoramica del linguaggio, cominciamo a vedere uno degli elementi più importanti della OOP, l’ereditarietà.

Riprendiamo l’esempio già visto della classe Persona:

class Persona {
    String nome;
    String cognome;

    public Persona(String nome, String cognome) {
        this.nome = nome;
        this.cognome = cognome;
    }

    public String toString() {
        return "Nome: " + this.nome + " - Cognome: " + this.cognome;
    }
}

Vogliamo adesso introdurre una nuova entità, l’impiegato. L’impiegato è un oggetto che rappresenta una persona, con però delle proprietà aggiuntive, un ruolo ed uno stipendio. Potremmo creare una nuova classe che riprende le caratteristiche di persona ed aggiunge le nuove proprietà. Ma di fatto creeremmo un secondo oggetto con caratteristiche in comune con quello già creato.

In alternativa possiamo introdurre il concetto di ereditarietà, cioè creare una classe figlia che eredita tutte le caratteristiche della classe genitore, ma che aggiunge nuove funzionalità. Vediamo:

class Impiegato extends Persona {
    String ruolo;
    double stipendio;

    public Impiegato(String nome, String cognome, String ruolo, double stipendio) {
        super(nome, cognome);
        this.ruolo = ruolo;
        this.stipendio = stipendio;
    }
}

Spiegazione:

  • la parola chiave extends indica la classe da cui si eredita;
  • la parola chiave super indica di richiamare un metodo della classe genitore (il costruttore). Super può essere usata anche per accedere a proprietà e metodi della classe genitore.

Se ora eseguiamo:

Impiegato impiegato = new Impiegato("Mario"; "Rossi", "contabile", 32000);
System.out.println(impiegato.nome);
System.out.println(impiegato.cognome);
System.out.println(impiegato.ruolo);
System.out.println(impiegato.stipendio);

Possiamo vedere che impiegato è a tutti gli effetti anche una persona, in quanto ne eredita tutte le caratteristiche. Possiamo anche riutilizzare i metodi della classe genitore, sempre con super:

class Impiegato extends Persona {
    String ruolo;
    double stipendio;

    public Impiegato(String nome, String cognome, String ruolo, double stipendio) {
        super(nome, cognome);
        this.ruolo = ruolo;
        this.stipendio = stipendio;
    }

    public String toString() {
        String result = super.toString() + "\n";
        result += "Ruolo: " + this.ruolo + " - Stipendio: " + this.stipendio;
        return result;
    }
}

L’ereditarietà un meccanismo potente che ci consente non solo di risparmiare codice, ma di organizzare il nostro progetto per componenti chiari e ben distinti mettendo a “fattor comune” proprietà di più classi. E questo lo vediamo proprio con questo esempio completo.


class Persona {
    String nome;
    String cognome;

    public Persona(String nome, String cognome) {
        this.nome = nome;
        this.cognome = cognome;
    }

    public String toString() {
        return "Nome: " + this.nome + " - Cognome: " + this.cognome;
    }
}

class Impiegato extends Persona {
    String ruolo;
    double stipendio;

    public Impiegato(String nome, String cognome, String ruolo, double stipendio) {
        super(nome, cognome);
        this.ruolo = ruolo;
        this.stipendio = stipendio;
    }

    public String toString() {
        String result = super.toString() + "\n";
        result += "Ruolo: " + this.ruolo + " - Stipendio: " + this.stipendio;
        return result;
    }
}

class Studente extends Persona {
    String scuola;
    String materiaPreferita;

    public Studente(String nome, String cognome, String scuola, String materiaPreferita) {
        super(nome, cognome);
        this.scuola = scuola;
        this.materiaPreferita = materiaPreferita;
    }

    public String toString() {
        String result = super.toString() + "\n";
        result += "Scuola: " + this.scuola + " - Materia preferita: " + this.materiaPreferita;
        return result;
    }
}

public class ApplicazionePersonaEreditarieta {
    public static void main(String[] args) {

        Impiegato impiegato = new Impiegato("Mario", "Rossi", "contabile", 32000);
        System.out.println(impiegato.toString());
        Studente studente = new Studente("Sara", "Bianchi", "ITT Informatica", "TPSI");
        System.out.println(studente.toString());
    }
}

Sia l’impiegato che lo studente sono persone, ed hanno quindi un genitore comune, che da loro le proprietà nome e cognome. Poi ognuna ha le sue peculiarità specifiche, come si vede nell’esempio.

Gerarchie di classi

Una classe può essere genitore di una seconda, che a sua volta è genitore di una terza e così via. Questo meccanismo consente di gestire in modo organizzato una gerarchia di classi, ognuna che specializza la classe padre.

Facciamo un esempio con una gerarchia di Conti Correnti:

Come si può vedere con la OOP otteniamo una struttura articolata che da una idea precisa del nostro modello. Questo tipo di schemi sono realizzati usando un linguaggio visuale noto come UML (Unified Modeling Language) che permette di creare diagrammi del programma che andremo a realizzare.

La realizzazione delle classi è lasciata come esercizio.

Come si può vedere in Java è possibile far ereditare da una classe genitore molte classi figlie, ma ogni classe può ereditare da una sola classe.

Classi final

Per impedire che una classe possa essere estesa da un’altra, si usa la parola chiave final:

public final class ContoProfessionista {... }

Le classi final sono utili per controllare ed impedire la creazione di una gerarchia con troppi livelli. Sono anche necessarie

Classi astratte

Una classe genitore è normalmente un modo per mettere a fattore comune proprietà e metodi delle classi figlie. Se è creata solo per questo, e non ha quindi senso creare istanze da questa classe, possiamo rendere la classe abstract, parola chiave che rende impossibile creare istanze da questa classe.

public abstract class ContoCorrente {
...
}

Questo impedisce errori in fase di creazione degli oggetti. emo ad usare, questa dovrà per forza implementare il metodo stampaDatiConto(), perché abbiamo dichiarato che nuovoConto è un ContoBancario, e contoBancario prevede un metodo stampaDatiConto().

Modificatore protected

Finora abbiamo visto i modificatori di accesso public (che consente di accedere ad un membro di una classe dall’esterno) e private (che invece impedisce questo accesso) in modo da poter controllare l’incapsulamento di un oggetto. Il problema è che il modificatore private impedisce di accedere ai membri dell’oggetto anche alle classi figlie, che quindi non possono usare funzioni della classe genitore con super.

Per risolvere questo problema c’è il modificatore protected, che si comporta come private verso l’esterno, ma consente l’accesso alle classi figlie. Ad esempio potremmo riscrivere la classe persona in questo modo:

class Persona {
    protected String nome;
    protected String cognome;

    public Persona(String nome, String cognome) {
        this.nome = nome;
        this.cognome = cognome;
    }

    public String toString() {
        return "Nome: " + this.nome + " - Cognome: " + this.cognome;
    }
}

Principi di programmazione ad oggetti

Scopo della OOP è fornire un valido strumento che aiuti il programmatore a progettare e realizzare una applicazione suddivisa in singole unità, dette oggetti, che garantiscano una suddivisione del problema in parti usabili (e riusabili). L’incapsulamento garantisce che gli oggetti contengano tutto il necessario per funzionare e siano protetti verso l’esterno, mentre l’ereditarietà consente di scrivere oggetti riutilizzando parti comuni.

Definiamo qui tre linee guida importanti che aiutano a svolgere questa progettazione e ci danno delle regole per gestire la suddivisione dell’applicazione in oggetti e come gestire incapsulamento ed ereditarietà.

Principio di singola responsabilità (SRP)

Un oggetto deve avere una ed una sola responsabilità. Questo significa che l’oggetto deve essere legato a risolvere un solo problema specifico. Se servono due oggetti per risolvere un problema specifico, conviene accorparli in uno solo. Se invece un oggetto ha più di una responsabilità, conviene scorporarlo in più oggetti eventualmente usando l’ereditarietà.

Ad esempio un conto bancario non può essere un conto deposito ed insieme un conto corrente. Hanno due responsabilità differenti. Mettiamo quindi a fattor comune gli elementi condivisi (conto bancario) e facciamo due classi distinte che ereditano da questo per le operazioni specifiche.

Invece accorpiamo nella stessa classe le funzioni di prelievo e versamento, perché sono parte della stessa responsabilità (gestire il movimento di denaro).

La SRP è il più importante principio della OOP, ma per comprenderlo pienamente occorre accumulare esperienza e imparare bene da progetti esistenti, verificando sempre bene se rispettano questo principio.

Principio Open Close (OCP)

Quando vogliamo aggiungere una funzionalità ad una classe esistente, se aggiunge una nuova responsabilità, è sempre meglio non modificarla (“close”, chiuso al cambiamento) ma estenderla con una sottoclasse (“open” aperto all’estensione).

Ad esempio un conto di un professionista richiede l’inserimento della partita iva. Non conviene modificare il conto corrente, perché è una nuova responsabilità, ma conviene estenderlo con una sottoclasse.

Questo principio evita di creare i cosiddetti “God object”, oggetti onnipotenti ed onniscienti che sanno tutto e fanno tutto, ma che sono anche complessi da utilizzare e modificare.

Conclusioni

In questa lezione abbiamo visto l’ereditarietà, un meccanismo OOP che ci consente di mettere a fattor comune metodi e proprietà di classi che fanno cose simili.

Grazie a questo sistema possiamo costruire gerarchie di classi collegate da una relazione di ereditarietà, e grazie a questo sviluppare applicazioni con un modello concettuale chiaro, senza ripetizione di codice.

Infine abbiamo visto due principi fondamentali della programmazione ad oggetti, il principio di singola responsabilità ed il principio open close.