Molti elementi, nella vita reale, hanno delle proprietà comuni. Ad esempio, sia i cani che i gatti sono animali. Anche
gli oggetti possono avere delle proprietà comuni, che è possibile chiarire utilizzando una generalizzazione tra le
relative classi. Estraendo le proprietà comuni in relative classi, sarà possibile modificare e gestire più facilmente
il sistema in futuro.
Una generalizzazione mostra che una classe eredita da un'altra. La classe di ereditarietà viene chiamata discendente.
La classe da cui si eredita viene chiamata antenato. Ereditarietà indica che la definizione dell'antenato, comprese le
proprietà come gli attributi, le relazioni o le operazioni sui relativi oggetti, è valida anche per gli oggetti del
discendente. La generalizzazione viene ottenuta dalla classe discendente alla relativa classe antenata.
La generalizzazione può avere luogo in varie fasi, il che consente la modellazione di gerarchie di ereditarietà a più
livelli e più complesse. Le proprietà generali vengono inserite nella parte superiore della gerarchia di ereditarietà e
le proprietà speciali più in basso. In altre parole, è possibile utilizzare la generalizzazione per modellare le
specializzazioni di un concetto più generale.
Esempio
Nel sistema Macchina per il riciclaggio, tutte le classi: Lattina, Bottiglia e Cassa, descrivono diversi tipi di voci
di deposito. Essi hanno due proprietà comuni, oltre a essere dello stesso tipo: ognuna ha un'altezza e un peso. E'
possibile modellare tali proprietà tramite gli attributi e le operazioni in una classe separata, Voce di deposito.
Lattina, Bottiglia e Cassa erediteranno le proprietà di questa classe.
Le classi Lattina, Bottiglia e Cassa hanno come proprietà comuni, peso e altezza. Ognuna è una specializzazione del
concetto generale Voce di deposito.
Una classe può ereditare da numerose altre classi tramite un'ereditarietà multipla, sebbene generalmente erediterà solo
da una.
Esistono un paio di problemi potenziali che occorre considerare se si utilizza l'ereditarietà multipla:
-
Se la classe eredita da diverse classi, è necessario controllare in che modo le relazioni, le operazioni e gli
attributi vengono indicati negli antenati. Se lo stesso nome compare in diversi antenati, è necessario descrivere
cosa significa per la specifica classe di ereditarietà, ad esempio, qualificando il nome per indicarne l'origine di
dichiarazione.
-
Se viene utilizzata l'ereditarietà ripetuta, in questo caso, lo stesso antenato viene ereditato da un discendente
più di una volta. Quando questo si verifica, la gerarchia di ereditarietà avrà una "forma di quadro", come mostrato
di seguito.
Ereditarietà multipla e ripetuta. La classe Scorrimento finestra con casella di dialogo eredita la classe Finestra più
di una volta.
Questo contesto potrebbe far sorgere la seguente domanda: "Quante copie degli attributi della Finestra sono incluse
nelle istanze di Scorrimento finestra con la casella di dialogo?" Se si utilizza un'ereditarietà ripetuta, occorre
avere una chiara definizione della relativa semantica; nella maggior parte dei casi, viene definita dal linguaggio di
programmazione che supporta l'ereditarietà multipla.
In generale, le regole del linguaggio di programmazione che governano l'ereditarietà multipla sono complesse e, spesso,
difficili da utilizzare correttamente. Di conseguenza, si consiglia di utilizzare l'ereditarietà multipla solo quando è
necessario e, sempre, con cautela.
Una classe non istanziata e che esiste solo affinché altre classi la ereditino, è una classe astratta. Le classi
istanziate realmente sono classi concrete. Tenere presente che una classe astratta deve avere almeno un discendente per
essere utile.
Esempio
Una Collocazione bancale nel Sistema di gestione magazzino è una classe di entità astratta che rappresenta le proprietà
comuni per differenti tipi di collocazioni dei bancali. La classe viene ereditata dalle classi concrete Stazione,
Trasportatore e Unità di immagazzinamento, che possono avere la funzione di collocazioni dei bancali nel magazzino.
Tutti questi oggetti hanno una proprietà comune: possono contenere uno o più bancali.
La classe ereditata, in questo caso Collocazione del bancale, è astratta e non istanziata per conto proprio.
Dal momento che gli stereotipi della classe hanno scopi differenti, l'ereditarietà da uno stereotipo della classe a un
altro non ha senso. L'ereditarietà di una classe boundary da parte di una classe di entità, ad esempio, renderebbe la
classe una sorta di ibrido. Quindi, si consiglia di utilizzare le generalizzazioni solo tra classi dello stesso
stereotipo.
E' possibile utilizzare la generalizzazione per esprimere due relazioni tra le classi:
-
Creazione di sottotipi, specificando che il discendente è un sottotipo dell'antenato. Creazione di sottotipi
significa che il discendente eredita la struttura e la funzionalità dell'antenato e che il discendente è un tipo
dell'antenato (ossia, che il discendente è un sottotipo che può sostituire tutti i relativi antenati in qualsiasi
situazione).
-
Creazione di sottoclassi, specificando che il discendente è una sottoclasse (ma non un sottotipo) dell'antenato.
Creazione di sottoclassi significa che il discendente eredita la struttura e la funzionalità dell'antenato e che il
discendente non è un tipo dell'antenato.
E' possibile creare delle relazioni come queste dividendo le proprietà comuni in diverse classi e inserendole in una
classe separata ereditata dalle altre o creando nuove classi che ne specializzano altre più generali e lasciando che
esse ereditino dalle classi generali.
Se le due varianti coincidono, non dovrebbe essere difficile impostare la giusta ereditarietà tra le classi. In alcuni
casi, tuttavia, non coincidono ed è necessario fare in modo che l'uso dell'ereditarietà sia comprensibile. Come minimo,
si dovrebbe conoscere lo scopo di ogni relazione di ereditarietà nel modello.
Creazione di sottotipi significa che il discendente è un sottotipo che può sostituire tutti i relativi antenati in
qualsiasi situazione. La creazione di sottotipi è un caso speciale di polimorfismo ed è una proprietà importante perché
consente di indicare tutti i client (oggetti che utilizzano l'antenato) senza prendere in considerazione i potenziali
discendenti dell'antenato. Ciò rende gli oggetti del client più generali e utilizzabili. Quando il client utilizza
l'oggetto reale, funziona in un modo specifico e rileva sempre che l'oggetto esegue la propria attività. La creazione
di sottotipi assicura che il sistema tollererà le modifiche nella serie di sottotipi.
Esempio
In un sistema di gestione magazzino, la classe Interfaccia del trasportatore definisce la funzionalità di base per la
comunicazione con tutti i tipi di attrezzature di trasporto, ad esempio le gru e i carrelli. La classe definisce, tra
le altre cose, l'operazione executeTransport.
Sia le classi Interfaccia del carrello che Interfaccia della gru eredita dall'Interfaccia del trasportatore; ossia, gli
oggetti di entrambe le classi risponderanno al messaggio executeTransport. Gli oggetti possono essere inseriti
nell'Interfaccia del trasportatore in qualsiasi momento e offriranno la relativa funzionalità. Quindi, altri oggetti
(oggetti client) possono inviare un messaggio a un oggetto dell'Interfaccia del trasportatore, senza sapere se un
oggetto Interfaccia del carrello o Interfaccia della gru risponderanno al messaggio.
La classe Interfaccia del trasportatore può anche essere astratta, mai istanziata di per sé. In tal caso, l'Interfaccia
del trasportatore può definire solo la firma dell'operazione executeTransport, mentre le classi discendenti la
implementano.
Alcuni linguaggi OO (object-oriented), come ad esempio C++, utilizzano la gerarchia di classe come gerarchia del tipo,
obbligando il progettista a utilizzare l'ereditarietà per creare sottotipi nel modello di progettazione. Altri, tra cui
Smalltalk-80, non hanno alcun controllo del tipo al momento della compilazione. Se gli oggetti non possono rispondere a
un messaggio ricevuto, genereranno un messaggio di errore.
Potrebbe essere una buona idea utilizzare la generalizzazione per indicare le relazioni del sottotipo anche in
linguaggi privi del controllo del tipo. In alcuni casi, sarebbe opportuno utilizzare la generalizzazione per rendere il
modello dell'oggetto e il codice sorgente più semplice da comprendere e gestire, indipendentemente dal fatto che il
linguaggio lo consenta o meno. Se questo uso dell'ereditarietà sia o meno un buon stile dipende fortemente dalle
convenzioni del linguaggio di programmazione.
La creazione di sottoclassi costituisce l'aspetto di riutilizzo della generalizzazione. Quando si creano le
sottoclassi, si considera quali parti di un'implementazione è possibile riutilizzare tramite ereditarietà delle
proprietà definite da altre classi. La creazione di sottoclassi consente di risparmiare lavoro e di riutilizzare il
codice durante l'implementazione di una determinata classe.
Esempio
Nella libreria di classi Smalltalk-80, la classe Dizionario eredita le proprietà dalla Serie.
La ragione per questa generalizzazione è che il Dizionario può riutilizzare alcuni metodi generali e strategie di
memorizzazione dall'implementazione della serie. Sebbene sia possibile visualizzare un Dizionario come Serie
(contenente coppie di valori chiave), Dizionario non è un sottotipo di Serie, perché non è possibile aggiungere
qualsiasi tipo di oggetto a un dizionario (solo coppie di valori chiave). Gli oggetti che utilizzano il Dizionario non
si rendono conto che, effettivamente, è una Serie.
La creazione di sottoclassi, spesso, conduce a gerarchie di ereditarietà illogiche, difficili da comprendere e da
gestire. Quindi, non si consiglia di utilizzare l'ereditarietà solo per il riutilizzo, a meno non venga fornita
un'altra indicazione nell'utilizzo del proprio linguaggio di programmazione. La gestione di tale tipo di riutilizzo è,
generalmente, piuttosto complessa. Qualsiasi modifica nella classe Serie potrebbe implicare modifiche considerevoli di
tutte le classi che ereditano la classe Serie. Considerare ciò ed ereditare solo classi stabili. L'ereditarietà
congelerà l'implementazione della classe Serie, perché le modifiche a tale classe sono troppo dispendiose.
L'uso delle relazioni di generalizzazione nella progettazione dovrebbe dipendere, in larga parte, dalla semantica e
dall'utilizzo consigliato dell'ereditarietà nel linguaggio di programmazione. A differenza dei linguaggi non OO
(object-oriented), quelli OO (object-oriented) supportano l'ereditarietà tra le classi. Si consiglia di gestire le
caratteristiche del linguaggio nel modello di progettazione. Se si utilizza un linguaggio che non supporta
l'ereditarietà o l'ereditarietà multipla, è necessario simulare l'ereditarietà nell'implementazione. In tal caso, è
meglio modellare la simulazione nel modello di progettazione e non utilizzare le generalizzazioni per descrivere le
strutture di ereditarietà. La modellazione delle strutture di ereditarietà con le generalizzazioni e la successiva
simulazione dell'ereditarietà nell'implementazione potrebbero danneggiare la progettazione.
Se si utilizza un linguaggio che non supporta l'ereditarietà o l'ereditarietà multipla, è necessario simulare
l'ereditarietà nell'implementazione. In questo caso, è meglio modellare la simulazione nel modello di progettazione e
non utilizzare le generalizzazioni per descrivere le strutture di ereditarietà. La modellazione delle strutture di
ereditarietà con le generalizzazioni e la successiva simulazione dell'ereditarietà nell'implementazione potrebbero
danneggiare la progettazione.
Probabilmente, sarà necessario modificare le interfacce e altre proprietà dell'oggetto durante la simulazione. Si
consiglia di simulare l'ereditarietà in uno dei seguenti modi:
-
Consentendo al discendente l'inoltro dei messaggi all'antenato.
-
Duplicando il codice dell'antenato in ogni discendente. In questo caso, non viene creata alcuna classe di antenato.
Esempio
In questo esempio, i discendenti inoltrano i messaggi all'antenato tramite i collegamenti che sono istanze di
associazioni.
La funzionalità comune degli oggetti Lattina, Bottiglia e Cassa viene assegnata a una classe speciale. Gli oggetti per
cui tale funzionalità è comune inviano un messaggio all'oggetto Voce di deposito per eseguire la funzionalità, quando è
necessario.
|