Ereditarietà (informatica)

Da Wikipedia, l'enciclopedia libera.

In informatica l'ereditarietà è uno dei concetti fondamentali nel paradigma di programmazione a oggetti. Essa consiste in una relazione che il linguaggio di programmazione, o il programmatore stesso, stabilisce tra due classi. Se la classe B eredita dalla classe A, si dice che B è una sottoclasse di A e che A è una superclasse di B. Denominazioni alternative equivalenti, sono classe madre o classe base per A e classe figlia o classe derivata per B. A seconda del linguaggio di programmazione, l'ereditarietà può essere ereditarietà singola o semplice (ogni classe può avere al più una superclasse diretta) o multipla (ogni classe può avere più superclassi dirette).

In generale, l'uso dell'ereditarietà dà luogo a una gerarchia di classi; nei linguaggi con ereditarietà singola, si ha un albero se esiste una superclasse "radice" unica di cui tutte le altre sono direttamente o indirettamente sottoclassi (come la classe Object nel caso di Java) o a una foresta altrimenti; l'ereditarietà multipla definisce invece una gerarchia a grafo aciclico diretto.

Interpretazionemodifica | modifica sorgente

L'ereditarietà è una relazione di generalizzazione/specificazione: la superclasse definisce un concetto generale e la sottoclasse rappresenta una variante specifica di tale concetto generale. Su questa interpretazione si basa tutta la teoria dell'ereditarietà nei linguaggi a oggetti. Oltre a essere un importante strumento di modellazione (e quindi significativo anche in contesti diversi dalla programmazione in senso stretto, per esempio in UML), l'ereditarietà ha importantissime ripercussioni sulla riusabilità del software.

Relazione is-a

Per esempio, data una classe telefono se ne potrebbe derivare la sottoclasse cellulare, poiché il cellulare è un caso particolare di telefono. Questo tipo di relazione viene detta anche relazione is-a ("è-un"): "un cellulare è-un telefono".

La relazione is-a che deve legare una sottoclasse alla sua superclasse viene spesso esplicitata facendo riferimento al cosiddetto principio di sostituzione di Liskov, introdotto nel 1993 da Barbara Liskov e Jeannette Wing. Secondo questo principio, gli oggetti appartenenti a una sottoclasse devono essere in grado di esibire tutti i comportamenti e le proprietà esibiti da quelli appartenenti alla superclasse, in modo tale che usarli in luogo di questi ultimi non alteri la correttezza delle informazioni restituite dal programma. Affinché la classe cellulare possa essere concepita come sottoclasse di telefono, per esempio, occorre che un cellulare possa essere usato in tutti i contesti in cui si richiede l'uso di un telefono.

Tanto la relazione is-a quanto il principio di Liskov non richiedono che la sottoclasse esponga solo le caratteristiche esibite dalla superclasse. Per esempio, il fatto che un cellulare possa anche inviare SMS non inficia il fatto che esso sia sostituibile a un telefono. Pertanto, la sottoclasse può esibire caratteristiche aggiuntive rispetto alla superclasse.
Inoltre, potrebbe anche eseguire in maniera differente alcune delle sue funzionalità, a patto che questa differenza non sia osservabile dall'esterno. Per esempio, un cellulare inizia o riceve una telefonata in modo tecnicamente diverso rispetto a un telefono tradizionale (utilizzando la rete GSM), ma anche questo non contraddice il principio di sostituibilità.

Violazione del principio di sostituibilità

Nonostante tutto, in genere è tecnicamente possibile estendere una classe violando il principio di sostituibilità, in quanto le regole imposte dal linguaggio di programmazione in uso non possono andare oltre la correttezza formale del codice scritto ed eventualmente la sua aderenza a determinate precondizioni o postcondizioni. In certi casi, il principio viene violato intenzionalmente[1]; tuttavia, quando succede, è opportuno che si documenti la cosa in modo appropriato, onde evitare che le istanze della classe siano usate dove si assume valido il citato principio di sostituibilità[1].

Polimorfismo
Exquisite-kfind.png Per approfondire, vedi Polimorfismo (informatica).

Quando il principio di sostituibilità è rispettato, l'ereditarietà può essere utilizzata per ottenere il cosiddetto polimorfismo. Se ben usato, esso permette di avere programmi flessibili, nel senso che permette di scrivere codice in grado di far fronte a necessità e modifiche future richiedendo correzioni minime e/o ben circoscritte.

Definizione tecnicamodifica | modifica sorgente

Il modo in cui i linguaggi di programmazione gestiscono le relazioni di ereditarietà consegue dal significato dato all'ereditarietà come relazione is-a. Una classe B dichiarata sottoclasse di un'altra classe A

  • eredita (ha implicitamente) tutte le variabili di istanza e tutti i metodi di A;
  • può avere variabili o metodi aggiuntivi;
  • può ridefinire i metodi ereditati da A attraverso l'overriding, in modo che essi eseguano la stessa operazione concettuale in un modo specializzato.

Il fatto che la sottoclasse erediti tutte le caratteristiche della superclasse ha senso proprio alla luce del concetto di sostituibilità. Nel paradigma object-oriented, infatti, una classe di oggetti risulta definita dalle sue caratteristiche (attributi e metodi). Di conseguenza, sarebbe falso affermare che "un cellulare è-un telefono" se il cellulare non avesse tutte le caratteristiche definitorie di un telefono (per esempio un microfono, un altoparlante, e la possibilità di iniziare o ricevere telefonate).

Quanto detto non implica, tuttavia, che la sostituibilità sia garantita: la relazione classe-sottoclasse deve essere concettualmente distinta dalla relazione tipo-sottotipo. In particolare, il meccanismo di overriding non garantisce che la semantica del metodo della superclasse resti inalterata nella sottoclasse. La sostituibilità non viene inoltre rispettata quando si utilizzano strumenti per l'occultamento di visibilità dei metodi (limitation).

Applicazioni dell'ereditarietàmodifica | modifica sorgente

L'ereditarietà può essere studiata e descritta da diversi punti di vista:

  • comportamento degli oggetti rispetto all'ambiente esterno;
  • struttura interna degli oggetti;
  • gerarchia dei livelli di ereditarietà;
  • impatto dell'ereditarietà sul software engineering.

In linea di massima, per evitare confusione, è consigliabile affrontare separatamente questi aspetti

Specializzazionemodifica | modifica sorgente

Exquisite-kfind.png Per approfondire, vedi Sottotipo (informatica).

Uno dei maggiori vantaggi dell'ereditarietà è la possibilità di creare versioni "specializzate" di classi già esistenti, cioè di crearne dei sottotipi. I costrutti che consentono di realizzare l'ereditarietà non garantiscono la specializzazione, a cui deve provvedere il programmatore definendo la sottoclasse nella maniera opportuna, in modo da rispettare il principio di sostituibilità.

Un altro meccanismo simile alla specializzazione è la specificazione: si ha quando una classe ereditata dichiara di possedere un determinato "comportamento" senza però implementarlo effettivamente: si parla in questo caso di classe astratta. Tutte le classi "concrete" (cioè non a loro volta astratte) che ereditano da questa classe astratta devono obbligatoriamente implementare quel particolare comportamento "mancante".

Ridefinizionemodifica | modifica sorgente

Exquisite-kfind.png Per approfondire, vedi Overriding.

Molti linguaggi di programmazione ad oggetti permettono ad una classe o ad un oggetto di modificare il modo in cui è implementata una propria funzionalità ereditata da un'altra classe (di solito un metodo). Questa caratteristica è chiamata "ridefinizione" (in inglese, overriding). A fronte di overriding, lo stesso metodo avrà un comportamento diverso se invocato sugli oggetti della superclasse o in quelli della sottoclasse (per lo meno nel caso dei linguaggi che adottano il binding dinamico). Ad esempio, data una classe Quadrilatero che definisce alcuni comportamenti generali per tutte le figure geometriche con 4 lati, la sottoclasse Rettangolo potrebbe ridefinire (ovvero "fare overriding di") quei metodi di Quadrilatero che possono essere reimplementati in maniera più specifica tenendo conto delle specificità dei rettangoli (per esempio, il calcolo dell'area potrebbe essere riscritto nella casse Rettangolo in modo più semplice ed efficiente semplicemente come prodotto dei lati).

Estensionemodifica | modifica sorgente

Un'altra ragione per usare l'ereditarietà è fornire ad una classe dati o funzionalità aggiuntive. Questa operazione è di solito chiamata estensione oppure subclassing. A differenza del caso della specializzazione prima esposto, con l'estensione nuovi dati o funzionalità sono aggiunti alla classe ereditata, accessibili ed utilizzabili da tutte le istanze della classe. L'estensione viene usata spesso quando non è possibile o conveniente aggiungere nuove funzionalità alla classe base. La stessa operazione può essere eseguita anche a livello di oggetto - anziché di classe - ad esempio usando i cosiddetti decorator pattern.

Riutilizzo del codicemodifica | modifica sorgente

Uno dei principali vantaggi dell'uso dell'ereditarietà (in particolare combinata col polimorfismo) è il fatto di favorire il riuso di codice. Non solo una sottoclasse eredita (e quindi riusa) il codice della superclasse: il polimorfismo garantisce anche che tutto il codice precedentemente scritto per manipolare oggetti della superclasse sia anche implicitamente in grado di manipolare oggetti della sottoclasse. Per esempio, un programma che sia in grado di rappresentare graficamente oggetti di classe Quadrilatero non avrebbe bisogno di alcuna modifica per trattare analogamente anche oggetti di una eventuale classe Rettangolo.

Esempimodifica | modifica sorgente

Supponiamo che in un programma si usi una classe Animale contenente dati per specificare, ad esempio, se l'animale è vivo, il luogo in cui si trova, quante zampe ha, ecc.; in aggiunta a questi dati la classe potrebbe contenere anche metodi per descrivere come l'animale mangia, beve, si muove, si accoppia, ecc. Se si volesse creare una classe Mammifero molte di queste caratteristiche rimarrebbero esattamente le stesse di quelle dei generici animali, ma alcune sarebbero diverse. Diremmo quindi che Mammifero è una sottoclasse della classe Animale (oppure, inversamente, che Animale è la classe classe base - chiamata anche classe genitrice - di Mammifero). La cosa importante da notare è che nel definire la nuova classe non è necessario specificare nuovamente che un mammifero ha le normali caratteristiche di un animale (luogo in cui si trova, il fatto che mangia, beve, ecc), ma basta aggiungere le caratteristiche peculiari che contraddistinguono i mammiferi rispetto agli altri animali (ad esempio, che è ricoperto di pelo e ha le mammelle, e ridefinire le funzioni che, pur essendo comuni a tutti gli altri animali, si manifestano in modo diverso, ad esempio il modo di riprodursi. Nell'esempio che segue, scritto in Java, notare all'interno del metodo riprodursi la chiamata a super.riprodursi(), che è un metodo della classe base che si sta ridefinendo. Per usare parole semplici si potrebbe dire che questo metodo dice di "fare prima tutto quello che la classe base farebbe" seguito poi dal codice che indica quali sono le "cose in più" che deve fare la nuova classe.

Javamodifica | modifica sorgente

class Mammifero extends Animale {
    Pelo pelo;
    Mammelle mammelle;
 
    Mammifero riproduciti() {
        Mammifero prole;
 
        super.riproduciti();
        if(isFemmina()) {
            prole = super.partorisci();
            prole.allatta(m_b);
        }
        curaCuccioli(prole);
        return prole;
    }
}

Fogli di stilemodifica | modifica sorgente

Il concetto di eredità si applica, più in generale, ad ogni processo dell'informatica in cui un determinato "contesto" riceve certe "caratteristiche" da un altro contesto. Ad esempio, in alcune applicazioni di elaborazione testi (word processor), gli attributi stilistici del testo come, dimensioni del font, layout o colore, possono essere ereditati da un template oppure da un altro documento. L'utente può definire attributi da applicare ad alcuni specifici elementi, mentre tutti gli altri ereditano gli attributi da una specifica di definizione globale degli stili. Ad esempio i cosiddetti Cascading Style Sheets (CSS) sono un linguaggio di definizione degli stili molto usato nella progettazione di pagine web. Anche in questo caso, alcuni attributi stilistici possono essere definiti in modo specifico, mentre altri sono ricevuti "in cascata". Quando si consultano siti web, per esempio, l'utente può decidere di applicare alle pagine uno stile definito da lui stesso per la grandezza dei font, mentre altre caratteristiche, come il colore ed il tipo dei font possono essere ereditati dal foglio di stile generale del sito.

Limitazioni ed alternativemodifica | modifica sorgente

Un uso massiccio della tecnica dell'ereditarietà nello sviluppo dei programmi può avere qualche controindicazione e porre alcuni vincoli.

Supponiamo di avere una classe Persona che contiene come dati nome, indirizzo, numero di telefono, età e sesso. Possiamo definire una sottoclasse di Persona, chiamata Studente, che contiene le medie dei voti ed i corsi frequentati, ed un'altra sottoclasse di Persona, chiamata Impiegato, che contiene il titolo di studio, la mansione svolta ed il salario.

Nella definizione di queste gerarchie di eredità sono già impliciti alcuni vincoli, alcuni dei quali sono utili, mentre altri creano problemi:

Vincoli posti dalla programmazione basata sull'ereditarietàmodifica | modifica sorgente

  • Unicità

Nel caso dell'eredità semplice, una classe può ereditare soltanto da una classe base. Nell'esempio sopra riportato, Persona può essere sia Studente che Impiegato, ma non entrambi. L'ereditarietà multipla risolve parzialmente questo problema, con la creazione di una classe StudenteImpiegato che eredita sia da Studente che da Impiegato. Tuttavia questa nuova classe può ereditare dalla rispettiva classe base solo una volta: questa soluzione, quindi, non risolve il caso in cui uno "studente" ha due lavori, oppure frequenta due scuole.

  • Staticità

La gerarchia dell'ereditarietà di un oggetto viene "congelata" nel momento in cui l'oggetto viene istanziato e non può più essere modificata successivamente. Per esempio, un oggetto della classe Studente non può diventare un oggetto Impiegato mantenendo le caratteristiche della sua classe base Personanon chiaro.

  • Visibilità

Quando un programma "client" ha accesso ad un oggetto, di solito ha accesso anche a tutti i dati di un oggetto appartenente alla classe base. Anche se la classe base non è di tipo "pubblico", il programma client può creare oggetti sul suo tipo. Per fare in modo che una funzione possa leggere il valore della media di uno Studente bisogna dare a questa funzione la possibilità di accedere anche a tutti i dati personali memorizzati nella classe base Persona.

Ereditarietà e ruolimodifica | modifica sorgente

Un ruolo descrive una caratteristica associata ad un oggetto in base alle interrelazioni che questo oggetto ha con un altro oggetto (ad esempio: una persona con il ruolo di studente frequenta un corso scolastico). L'ereditarietà può essere usata per implementare queste relazioni. Nella programmazione orientata agli oggetti spesso queste due tecniche di programmazione sono usate in alternativa fra di loro. Spesso si usa l'eredità per modellare i ruoli. Ad esempio, si può definire un ruolo Studente per una Persona realizzato definendo una sottoclasse di Persona. In ogni caso, né la gerarchia dell'eredità, né il tipo degli oggetti può variare nel tempo. Per questo motivo definire i ruoli come sottoclassi può causare il congelamento dei ruoli al momento della creazione dell'oggetto. Nel nostro esempio Persona non potrebbe più cambiare facilmente il suo ruolo da Studente ad Impiegato, se le circostanze lo richiedessero.

Queste restrizioni possono essere dannose, in quanto rendono più difficili da implementare le modifiche che in futuro dovessero rendersi necessarie, in quanto queste ultime potranno essere introdotte solo previa rimodellazione ed aggiornamento dell'intero progetto.

Per fare un uso corretto dell'ereditarietà bisogna ragionare in termini quanto più possibile "generali", in modo che gli aspetti comuni alla maggior parte delle classi da istanziare siano riuniti "a fattor comune" ed inseriti nelle rispettive classi genitrici. Per esempio una classe base AspettiLegali può essere ereditata sia dalla classe Persona che dalla classe Ditta per gestire le problematiche legali comuni ad entrambi.

Per scegliere la tecnica più conveniente da applicare (progetto basato sui ruoli oppure sull'eredità) conviene chiedersi se:

  • uno stesso oggetto deve rappresentare ruoli diversi e svolgere funzionalità diverse in tempi diversi (progettare in base ai ruoli);
  • più classi (nota bene, classi, NON oggetti) devono svolgere operazioni comuni che possono essere raggruppate ed attribuite ad un'unica classe base (progettare in base all'ereditarietà).

Una conseguenza importante della separazione fra ruoli e classi genitrici è che il compile-time ed il run-time del codice oggetto prodotto sono nettamente separati. L'ereditarietà è chiaramente un costrutto che si applica compile-time, che non modifica la struttura degli oggetti durante il run-time. Infatti i "tipi" degli oggetti istanziati sono già predeterminati durante il compile-time. Come già indicato negli esempi precedenti, quando si progetta la classe Persona, essendo un impiegato un caso particolare di persona, bisogna assicurarsi che la classe Persona contenga solo le funzionalità ed i dati comuni a tutte le persone, indipendentemente dal contesto in cui questa classe viene istanziata. In questo modo si è sicuri, ad esempio, che in una classe Persona non verrà mai usato il membro Lavoro, poiché non tutte le persone hanno un lavoro, o, per lo meno, non è garantito a priori che la classe Persona sia istanziata solo per creare oggetti riferibili a persone che hanno un lavoro.

Invece, ragionando dal punto di vista della programmazione basata sui ruoli, si potrebbe definire un sottoassieme di tutti i possibili oggetti persona che svolgono il "ruolo" di impiegato. Le informazioni necessarie a definire le caratteristiche del lavoro svolto verranno inserite solo negli oggetti che svolgono il ruolo di impiegato.

Una modellazione orientata agli oggetti potrebbe definire il Lavoro stesso come ruolo, poiché un lavoro può essere svolto anche soltanto temporaneamente, e quindi non ha le caratteristiche di "stabilità" richieste per modellare su di esso una classe. Al contrario il concetto di PostoDiLavoro è dotato di caratteristiche di stabilità e persistenza nel tempo. Di conseguenza, ragionando in un'ottica di programmazione ad oggetti, si potrebbe costruire una classe Persona ed una classe PostoDiLavoro, che interagiscono fra loro secondo una relazione del tipo molti-a-molti con lo schema "lavora-in", dove una Persona riveste il ruolo di impiegato, quando ha un impiego, e dove, simmetricamente, l'impiego riveste il ruolo del "suo posto di lavoro" quando l'impiegato lavora al suo interno.

Notare che con questo approccio tutte le classi sono create all'interno di un unico "dominio", nel senso che descrivono entità riconducibili ad un unico ambito per quanto riguarda la terminologia che le descrive, cosa non possibile nel caso si usino approcci di altro tipo.

La differenza fra ruoli e classi è difficile da capire se si adottano costrutti e funzioni dotati di trasparenza referenziale - vale a dire costrutti e funzioni che, quando ricevono in input lo stesso parametro restituiscono sempre lo stesso valore - poiché i ruoli sono tipi accessibili "per riferimento", mentre le classi sono tipi accessibili solo quando vengono istanziate in oggetti.

Programmazione orientata ai componenti come alternativa all'ereditarietàmodifica | modifica sorgente

La programmazione orientata ai componenti offre un metodo alternativo per descrivere e manipolare il sistema sopra descritto di persone, studenti ed impiegati, ad esempio definendo un insieme di classi ausiliarie Iscrizione e PostoDiLavoro per immagazzinare le informazioni necessarie a descrivere rispettivamente lo studente e l'impiegato. A ciascun oggetto Persona si può quindi associare una collezione di oggetti PostoDiLavoro. Questo modo di procedere risolve alcuni dei problemi sopra menzionati:

  • una Persona può ora avere un numero qualsiasi di posti di lavoro e frequentare un numero qualsiasi di istituti scolastici;
  • tutti questi posti di lavoro possono ora essere cambiati, aggiunti ed eliminati in modo dinamico;
  • è ora possibile passare un oggetto Iscrizione come parametro di una funzione - per esempio di una funzione che deve decidere se una domanda di iscrizione viene accolta - senza dover passare come parametri tutti i dati che specificano i dati personali (nome, età, indirizzo, ecc.)

L'uso dei componenti al posto dell'ereditarietà produce anche codice scritto con una sintassi meno ambigua e più facile da interpretare. Confrontare i due esempi seguenti: nel primo si usa l'ereditarietà:

Impiegato i = getImpiegato();
print(i.mansioneDiLavoro());

È chiaro che la funzione mansioneDiLavoro() è definita nella classe Impiegato, ma potrebbe essere definita anche nella classe base Persona, e ciò potrebbe provocare ambiguità. Con la programmazione a componenti il programmatore può ridurre le ambiguità applicando una gerarchia di eredità più "piatta":

Persona p = getPersona();
print(p.impiego().mansione());

Sapendo che la classe Impiego non ha classi genitrici, è immediatamente ovvio che la funzione mansione() è definita nella classe Impiego

La programmazione orientata ai componenti, tuttavia, non può essere sempre un'alternativa valida a quella basata sull'ereditarietà, che, ad esempio, consente il polimorfismo e l'incapsulamento. Inoltre la creazione di classi di componenti può aumentare anche di molto la lunghezza del codice da scrivere.

Notemodifica | modifica sorgente

  1. ^ a b Esempio in Java: la classe java.util.IdentityHashMap, appartenente alle librerie standard del linguaggio, viola intenzionalmente il contratto generale stabilito dal tipo java.util.Map, ma, come si vede dalla documentazione della stessa, il fatto che il contratto generale dell'interfaccia Map sia violato è ben documentato.

Voci correlatemodifica | modifica sorgente








Creative Commons License