Ereditarietà (informatica)

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 padre, 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" di cui tutte le altre classi 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.

Interpretazione

[modifica | modifica wikitesto]

L'ereditarietà è una relazione di generalizzazione/specializzazione: 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

[modifica | modifica wikitesto]

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 soddisfi tutte le caratteristiche che definiscono genericamente un telefono.

Tanto la relazione is-a quanto il principio di Liskov non richiedono che la sottoclasse esponga solo le caratteristiche esibite dalla superclasse, ma che esponga almeno tali caratteristiche. 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.
Una sottoclasse può anche eseguire in maniera differente rispetto alla superclasse alcune delle sue funzionalità, a patto che questa differenza non sia osservabile dall'esterno. Per esempio, un cellulare svolge le telefonate in modo tecnicamente diverso rispetto a un telefono tradizionale (utilizzando la rete GSM), ma dal punto di vista esteriore si ottiene lo stesso risultato (eseguire una telefonata), pertanto ciò non contraddice il principio di sostituzione.

Violazione del principio di sostituibilità

[modifica | modifica wikitesto]

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]; quando succede è opportuno che si documenti la cosa in modo appropriato, onde evitare che le istanze della classe siano usate dove è necessario che sia valido il citato principio di sostituibilità[1].

Lo stesso argomento in dettaglio: 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 tecnica

[modifica | modifica wikitesto]

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 come sottoclasse di una superclasse A:

  • eredita (ha implicitamente) tutte le variabili di istanza e tutti i metodi di A.
  • può avere (non necessariamente) 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 che definiscono un telefono (per esempio un microfono, un altoparlante e la possibilità di inoltrare e 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 wikitesto]

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

Specializzazione

[modifica | modifica wikitesto]
Lo stesso argomento in dettaglio: 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".

Ridefinizione

[modifica | modifica wikitesto]
Lo stesso argomento in dettaglio: Overriding.

Molti linguaggi di programmazione a oggetti permettono a una classe o a 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" o con il termine 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 del perimetro potrebbe essere riscritto nella classe Rettangolo come doppio della somma della lunghezza delle due basi, anziché come semplice somma delle lunghezze dei lati).

Un'altra ragione per usare l'ereditarietà è fornire a 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 e 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 codice

[modifica | modifica wikitesto]
Lo stesso argomento in dettaglio: Riuso di codice.

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.

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 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 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 riproduciti() la chiamata a super.riproduciti(), 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.

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;     } } 

Nell'esempio sottostante, viene dichiarata una classe Impiegato con alcuni attributi (Variabili) comuni. Viene dichiarato il costruttore (Sub) grazie al quale si potrà instanziare un oggetto di classe impiegato. Le variabili indicate con "_" servono a fare in modo di poter inserire eventualmente validazione dei dati prima di passare effettivamente i valori in input dentro l'oggetto. Di sotto invece, la classe manager eredita dalla classe Impiegato. Avrà quindi ottenuto (o meglio ereditato) implicitamente tutti i metodi e le funzioni che abbiamo dichiarato nella classe padre. In questo esempio pratico possiamo osservare che la classe manager oltre a ereditare le proprietà della classe impiegato, implementa funzioni e parametri esclusivi.

Public Class Impiegato     private nome as String     private salario as Double     private matricola as String     private anniDiServizio as Integer     Public Sub New(n As String, s as Double, m as String, ads as Integer)         nome = _Nome as string           salario = _salario as double          matricola = _matricola as string          anniDiServizio = _ads as integer               End Sub    end class  'La classe Manager che EREDITA dalla classe Impiegato  Public Class Manager Inherits Impiegato     Private nomeSegretaria as String     Public Sub New(n as String, s as Double, m asString, ads as Integer)         MyBase.New(n, s, m, ads)         nomeSegretaria = String.empty     End Sub    End Class  'Ldp' 

Fogli di stile

[modifica | modifica wikitesto]

Il concetto di eredità si applica, più in generale, a 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 e il tipo dei font possono essere ereditati dal foglio di stile generale del sito.

Limitazioni e alternative

[modifica | modifica wikitesto]

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 e i corsi frequentati, e un'altra sottoclasse di Persona, chiamata Impiegato, che contiene il titolo di studio, la mansione svolta e 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 wikitesto]

Nel caso dell'eredità semplice, una classe può ereditare soltanto da una classe base. Nell'esempio sopra riportato, un'istanza di Persona può essere o Studente oImpiegato, non entrambi contemporaneamente. 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.

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 Persona[non chiaro].

Lo stesso argomento in dettaglio: Visibilità (programmazione).

Quando un programma "client" ha accesso a 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 ruoli

[modifica | modifica wikitesto]

Un ruolo descrive una caratteristica associata a 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 a 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 e 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" e 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 a 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 e attribuite a un'unica classe base (progettare in base all'ereditarietà).

Una conseguenza importante della separazione fra ruoli e classi genitrici è che il compile-time e 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à e 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 a oggetti, si potrebbe costruire una classe Persona e 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 a 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 wikitesto]

La programmazione orientata ai componenti offre un metodo alternativo per descrivere e manipolare il sistema sopra descritto di persone, studenti e 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.

  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 correlate

[modifica | modifica wikitesto]

Collegamenti esterni

[modifica | modifica wikitesto]
Controllo di autoritàGND (DE4277478-0
  Portale Informatica: accedi alle voci di Wikipedia che trattano di informatica