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].
Polimorfismo
[modifica | modifica wikitesto]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]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]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).
Estensione
[modifica | modifica wikitesto]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]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
.
Esempi
[modifica | modifica wikitesto]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.
Java
[modifica | modifica wikitesto]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.
VB.NET
[modifica | modifica wikitesto]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]Unicità
[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.
Staticità
[modifica | modifica wikitesto]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].
Visibilità
[modifica | modifica wikitesto]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.
Note
[modifica | modifica wikitesto]- ^ a b Esempio in Java: la classe
java.util.IdentityHashMap
, appartenente alle librerie standard del linguaggio, viola intenzionalmente il contratto generale stabilito dal tipojava.util.Map
, ma, come si vede dalla documentazione della stessa, il fatto che il contratto generale dell'interfacciaMap
sia violato è ben documentato.
Voci correlate
[modifica | modifica wikitesto]- Ereditarietà multipla
- Classe (informatica)
- Classe astratta
- Interfaccia (informatica)
- Polimorfismo (informatica)
- Incapsulamento (informatica)
- Principio aperto/chiuso
Collegamenti esterni
[modifica | modifica wikitesto]- (EN) Denis Howe, inheritance, in Free On-line Dictionary of Computing. Disponibile con licenza GFDL
Controllo di autorità | GND (DE) 4277478-0 |
---|