Ereditarietà (informatica)
Da Wikipedia, l'enciclopedia libera.
| Questa voce o sezione di informatica non riporta fonti o riferimenti.
Puoi migliorare questa voce aggiungendo citazioni da fonti attendibili, secondo le linee guida sull'uso delle fonti.
|
| Questa voce o sezione di informatica è ritenuta da controllare. Motivo: Trattazione ricca di imprecisioni e lacune (vedi discussione)
Partecipa alla discussione e/o correggi la voce.
|
L' ereditarietà è uno dei concetti fondamentali nel paradigma di programmazione a oggetti. Essa consente di definire una classe come sottoclasse o classe derivata a partire da una classe preesistente detta superclasse o classe base. La sottoclasse "eredita" implicitamente tutte le caratteristiche (attributi e operazioni) della classe base.
Concettualmente, l'ereditarietà indica una relazione di generalizzazione: essa corrisponde infatti all'idea che la superclasse rappresenti un concetto generale e la sottoclasse rappresenti una variante specifica di tale concetto generale. Su questa interpretazione generale si basa tutta la teoria dell'ereditarietà nei linguaggi a oggetti, che include concetti come il polimorfismo, l'overriding e le classi astratte. 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.
Indice |
[modifica] Interpretazione concettuale
L'ereditarietà viene intesa come una rappresentazione di relazioni di generalizzazione/specializzazione fra classi. Per esempio, data una classe telefono se ne potrebbe derivare la sottoclasse cellulare, intendendo che il concetto di "cellulare" è una specializzazione (un caso particolare) del concetto generale di "telefono". Questo tipo di relazione viene detta anche relazione ISA, dall'inglese "is a" ("è-un"): "un cellulare è-un telefono". Il fatto che la sottoclasse erediti tutte le caratteristiche della superclasse ha senso proprio alla luce di questa interpretazione. Nel paradigma object-oriented, infatti, una classe di oggetti è 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).
La relazione ISA che deve legare una sottoclasse alla sua superclasse viene spesso esplicitata facendo riferimento al cosiddetto principio di sostituzione di Liskov (Liskov substitution principle, LSP) 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 da poter essere "sostituiti" liberamente a questi ultimi. 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 ISA che il principio di Liskov non richiedono che la sottoclasse abbia 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à.
[modifica] Definizione tecnica
Il modo in cui i linguaggi di programmazione gestiscono le relazioni di ereditarietà è coerente con la rappresentazione concettuale dell'ereditarietà come relazione ISA o sostituibilità. Una classe A dichiarata sottoclasse di un'altra classe B:
- eredita (ha implicitamente) tutte le variabili di istanza e tutti i metodi di B;
- può avere variabili o metodi aggiuntivi;
- può ridefinire i metodi ereditati da B attraverso l'overriding, in modo tale che essi eseguano la stessa operazione concettuale in un modo specializzato.
[modifica] Applicazioni dell'ereditarietà
L'ereditarietà può essere studiata e descritta da diverse angolazioni, e mettendo il focus su aspetti come:
- 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
[modifica] Specializzazione
Uno dei maggiori vantaggi dell'ereditarietà è la possibilità di creare versioni "specializzate"" di classi od oggetti già esistenti. Quando è riferita a classi questa operazione è chiamata subtyping. La nuova classe (od oggetto) aggiunge nuovi dati e funzionalità a quelle della classe da cui eredita. Per esempio una classe "Conto Bancario" può contenere dati che indicano "numero di conto", "titolare", "saldo". Una classe "Conto Bancario Fruttifero" che eredita da "Conto Bancario", oltre a tutti questi dati ereditati, può contenere come dati aggiuntivi "tasso di interesse" e "interessi maturati", e, inoltre, le funzionalità per il calcolo degli interessi.
Un'altra forma di specializzazione 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 che ereditano da questa classe astratta devono obbligatoriamente implementare quel particolare comportamento "mancante". Questo meccanismo di implementazione di un comportamento indefinito nella classe da cui si eredita viene chiamato anche reificazione
[modifica] Ridefinizione
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, "override"). La ridefinizione causa una complicazione: quale versione del metodo è "visto" dalla classe ereditata: quello "originario" - che fa pure parte automaticamente della stessa classe - oppure quello ridefinito? La risposta varia a seconda dei linguaggi, alcuni dei quali offrono al programmatore la possibilità di dichiarare esplicitamente se un dato metodo può essere ridefinito.
[modifica] Estensione
Un'altra ragione per usare l'ereditarietà è fornire ad una classe dati o funzionalità aggiuntive. Questa operazione è di solito chamata 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.
[modifica] Riutilizzo del codice
Uno dei principali vantaggi dell'uso dell'ereditarietà è la possibilità di creare nuove classi riutilizzando codice già esistente e collaudato di altre classi, realizzando la cosiddetta implementazione dell'ereditarietà. Va detto, tuttavia, che questa motivazione sta gradualmente venendo a mancare. Infatti questa tecnica ha lo svantaggio di non dare alcuna garanzia di intercambiabilità del funzionamento polimorfico, nel senso che un'istanza della classe base non è detto possa essere compatibile con un'istanza della classe ereditata. In alternativa si usa a volte la tecnica dei delegation pattern, che richiede un maggior lavoro di scrittura di codice ma risolve questi problemi di intercambiabilità.
[modifica] Esempi
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.
[modifica] Java
class Mammifero extends Animale {
Pelo m_h;
Mammelle m_b;
Mammifero riprodursi() {
Mammifero prole;
super.riprodursi();
if (è_femmina()) {
prole = super.partorire();
prole.allattare(m_b);
}
curare_i_cuccioli(prole);
return prole;
}
}
[modifica] Fogli di stile
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 i 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.
[modifica] Limitazioni ed alternative
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:
[modifica] Vincoli posti dalla programmazione basata sull'ereditarietà
- 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 Persona.
- 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.
[modifica] Ereditarietà e ruoli
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 da 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.
[modifica] Programmazione orientata ai componenti come alternativa all'ereditarietà
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
Personapuò 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
Iscrizionecome 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.

