Utilizzo dell'ereditarietà
Aggiornamento: novembre 2007
L'ereditarietà è un concetto di programmazione molto utile, ma che si presta facilmente a un utilizzo improprio. Spesso la stessa funzione può essere svolta più efficacemente dalle interfacce. Questo argomento, insieme a Quando utilizzare le interfacce, consente di comprendere quando sia più opportuno utilizzare un approccio rispetto all'altro.
L'ereditarietà è da preferire quando:
La gerarchia di ereditarietà rappresenta una relazione di tipo "è" piuttosto che una relazione di tipo "ha".
È possibile riutilizzare il codice delle classi base.
È necessario applicare la stessa classe e gli stessi metodi a tipi di dati diversi.
La gerarchia di ereditarietà è ragionevolmente superficiale e si prevede che gli sviluppatori non vi aggiungeranno molti altri livelli.
Si desidera apportare modifiche globali alle classi derivate modificando una classe base.
Queste considerazioni vengono esaminate in ordine di seguito.
Ereditarietà e relazioni di tipo "è"
Le relazioni tra le classi nella programmazione orientata ad oggetti possono essere espresse in due modi: con relazioni di tipo "è" e "ha". In una relazione di tipo "è" la classe derivata è chiaramente un tipo della classe base. Una classe denominata PremierCustomer, ad esempio, rappresenta una relazione di tipo "è" con una classe base denominata Customer, perché un cliente principale è un cliente. Una classe denominata CustomerReferral, invece, rappresenta una relazione di tipo "ha" con la classe Customer, perché il riferimento di un cliente ha un cliente, ma non è un tipo di cliente.
È necessario che agli oggetti di una gerarchia di ereditarietà sia associata una relazione di tipo "è" con la relativa classe base, perché ne ereditano i campi, le proprietà, i metodi e gli eventi. Le classi che rappresentano una relazione di tipo "ha" con altre classi non sono adatte alle gerarchie di ereditarietà perché possono ereditare proprietà e metodi inappropriati. Se la classe CustomerReferral fosse derivata dalla classe Customer esaminata in precedenza, ad esempio, potrebbero essere ereditate proprietà non pertinenti, come ShippingPrefs o LastOrderPlaced. È necessario che le relazioni di tipo "ha" come questa vengano rappresentate mediante classi non correlate o interfacce. Nella seguente illustrazione vengono riportati esempi sia della relazione di tipo "è" che della relazione di tipo "ha".
Classi base e riutilizzo del codice
Un altro vantaggio dell'ereditarietà consiste nella possibilità di riutilizzare il codice. Le classi, se progettate correttamente, possono essere sottoposte a debug una sola volta e quindi utilizzate ripetutamente come base per nuove classi.
Un esempio comune di riutilizzo efficace del codice è la connessione con le librerie che gestiscono strutture di dati. Si supponga, ad esempio, di disporre di una grande applicazione aziendale che gestisce numerosi tipi di elenchi in memoria. Uno di questi elenchi è una copia in memoria del database dei clienti, letto da un database all'inizio della sessione per maggiore rapidità. La struttura dati potrebbe essere analoga alla seguente:
Class CustomerInfo
Protected PreviousCustomer As CustomerInfo
Protected NextCustomer As CustomerInfo
Public ID As Integer
Public FullName As String
Public Sub InsertCustomer(ByVal FullName As String)
' Insert code to add a CustomerInfo item to the list.
End Sub
Public Sub DeleteCustomer()
' Insert code to remove a CustomerInfo item from the list.
End Sub
Public Function GetNextCustomer() As CustomerInfo
' Insert code to get the next CustomerInfo item from the list.
Return NextCustomer
End Function
Public Function GetPrevCustomer() As CustomerInfo
'Insert code to get the previous CustomerInfo item from the list.
Return PreviousCustomer
End Function
End Class
Nell'applicazione potrebbe essere presente inoltre un elenco simile di prodotti che l'utente ha aggiunto all'elenco di un carrello, come indicato nel frammento di codice seguente:
Class ShoppingCartItem
Protected PreviousItem As ShoppingCartItem
Protected NextItem As ShoppingCartItem
Public ProductCode As Integer
Public Function GetNextItem() As ShoppingCartItem
' Insert code to get the next ShoppingCartItem from the list.
Return NextItem
End Function
End Class
È possibile individuare delle analogie: i due elenchi consentono lo stesso tipo di operazioni, ad esempio di inserimento, eliminazione e recupero, ma i tipi di dati utilizzati sono diversi. Gestire due basi di codice per eseguire sostanzialmente le stesse funzioni non è efficiente. La soluzione più efficace in questo caso consiste nell'eseguire la gestione degli elenchi nella classe di appartenenza e quindi ereditare da quella classe i diversi tipi di dati:
Class ListItem
Protected PreviousItem As ListItem
Protected NextItem As ListItem
Public Function GetNextItem() As ListItem
' Insert code to get the next item in the list.
Return NextItem
End Function
Public Sub InsertNextItem()
' Insert code to add a item to the list.
End Sub
Public Sub DeleteNextItem()
' Insert code to remove a item from the list.
End Sub
Public Function GetPrevItem() As ListItem
'Insert code to get the previous item from the list.
Return PreviousItem
End Function
End Class
È necessario sottoporre la classe ListItem a debug una sola volta. Successivamente sarà possibile creare classi che la utilizzano senza preoccuparsi ancora della gestione degli elenchi. Di seguito è riportato un esempio:
Class CustomerInfo
Inherits ListItem
Public ID As Integer
Public FullName As String
End Class
Class ShoppingCartItem
Inherits ListItem
Public ProductCode As Integer
End Class
Il riutilizzo del codice basato sull'ereditarietà, pur essendo uno strumento efficace, comporta alcuni rischi. Anche nei sistemi progettati con la massima cura a volte si verificano cambiamenti non prevedibili dai programmatori. Le modifiche intervenute in una gerarchia di classi esistente a volte possono avere conseguenze impreviste. Nella sezione "Il problema della fragilità della classe base" dell'argomento Modifica della progettazione delle classi base dopo la distribuzione sono illustrati alcuni esempi.
Classi derivate intercambiabili
Talvolta le classi derivate di una gerarchia di classi possono essere utilizzate in modo intercambiabile con la classe di base. Questo processo è denominato polimorfismo basato sull'ereditarietà. Questo approccio combina le caratteristiche migliori del polimorfismo basato sulle interfacce con la possibilità di riutilizzare il codice di una classe base o di eseguirne l'override.
Il polimorfismo può risultare utile ad esempio in un pacchetto grafico. Si consideri, ad esempio, il seguente frammento di codice che non utilizza l'ereditarietà:
Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _
ByVal Y As Integer, ByVal Size As Integer)
Select Case Shape.type
Case shpCircle
' Insert circle drawing code here.
Case shpLine
' Insert line drawing code here.
End Select
End Sub
Questo approccio pone alcuni problemi. Se successivamente si decide di aggiungere un'opzione ellisse, sarà necessario modificare il codice sorgente, che potrebbe persino non essere accessibile agli utenti di destinazione. Un problema più insidioso riguarda il fatto che, poiché le ellissi hanno sia un diametro maggiore che un diametro minore, per disegnare un'ellisse è necessario un altro parametro, che è irrilevante nel caso di una linea. Se poi si volesse aggiungere una polilinea, vale a dire più linee collegate, sarebbe necessario aggiungere un altro parametro che è irrilevante negli altri casi.
Gran parte di questi problemi è risolta dall'ereditarietà. Le classi base progettate correttamente demandano l'implementazione dei metodi specifici alle classi derivate, consentendo la gestione di qualsiasi tipo di forma. Gli altri sviluppatori possono implementare i metodi nelle classi derivate utilizzando la documentazione per la classe base. Altri elementi della classe, come le coordinate x e y, possono essere creati nella classe base perché vengono utilizzati da tutti i discendenti. Draw ad esempio potrebbe essere un metodo di tipo MustOverride:
MustInherit Class Shape
Public X As Integer
Public Y As Integer
MustOverride Sub Draw()
End Class
A questa classe potrebbero poi essere fatte altre aggiunte per le diverse forme. Per una classe Line ad esempio sarà necessario solo il campo Length:
Class Line
Inherits Shape
Public Length As Integer
Overrides Sub Draw()
' Insert code here to implement Draw for this shape.
End Sub
End Class
Questo approccio è utile perché altri sviluppatori, che non hanno accesso al codice sorgente, possono estendere la classe base con nuove classi derivate secondo le necessità. Dalla classe Line è ad esempio possibile derivare una classe Rectangle.
Class Rectangle
Inherits Line
Public Width As Integer
Overrides Sub Draw()
' Insert code here to implement Draw for the Rectangle shape.
End Sub
End Class
In questo esempio viene illustrato come sia possibile passare da classi per scopi generici a classi molto specifiche, aggiungendo dettagli di implementazione a ogni livello.
A questo punto è necessario riconsiderare se la classe derivata rappresenta realmente una relazione di tipo "è" oppure costituisce una relazione di tipo "ha". Se la nuova classe rettangolo è semplicemente costituita da linee, l'ereditarietà non costituirà più la scelta migliore. Se, tuttavia, il nuovo rettangolo è una linea con una proprietà larghezza, la relazione di tipo "è" è stata mantenuta.
Gerarchie di classi superficiali
L'ereditarietà è più adatta alle gerarchie di classi relativamente superficiali. Le gerarchie di classi eccessivamente articolate e complesse possono essere difficili da sviluppare. Per decidere se utilizzare una gerarchia di classi, è necessario soppesare i vantaggi di questa scelta a fronte della complessità. In generale è buona norma limitare le gerarchie a sei livelli o meno. Il numero massimo di livelli di una particolare gerarchia di classi, tuttavia, dipende da una serie di fattori, tra cui il grado di complessità di ciascun livello.
Modifiche globali a classi derivate tramite la classe base
Una delle funzioni più efficaci dell'ereditarietà è la capacità di apportare modifiche a una classe base che si propagano alle classi derivate. Questa funzione, utilizzata con cautela, consente di aggiornare l'implementazione di un singolo metodo consentendo a decine o persino centinaia di classi derivate di utilizzare il nuovo codice. Questa pratica, tuttavia, può essere pericolosa perché le modifiche possono generare problemi con le classi ereditate progettate da altri. È necessario pertanto accertarsi che la nuova classe base sia compatibile con le classi che utilizzano quella originale. È in particolare necessario evitare di cambiare il nome o il tipo dei membri della classe base.
Si supponga, ad esempio, di progettare una classe base con un campo di tipo Integer per memorizzare le informazioni relative ai codici postali e che altri sviluppatori abbiano creato classi derivate che utilizzano il campo ereditato del codice postale. Si supponga inoltre che nel campo del codice postale vengano registrate cinque cifre e che l'ufficio postale abbia esteso i codici aggiungendo un trattino e altre quattro cifre. In uno scenario del caso peggiore, è possibile modificare il campo nella classe base in modo che accetti una stringa di dieci caratteri. Sarà quindi necessario che gli sviluppatori modifichino e ricompilino le classi derivate per utilizzare le nuove dimensioni e il nuovo tipo di dati.
Il modo più sicuro per modificare una classe base è semplicemente quello di aggiungere nuovi membri. Nel caso esaminato in precedenza, ad esempio, è possibile aggiungere un nuovo campo per registrare quattro cifre supplementari. In questo modo, le applicazioni client possono essere aggiornate per utilizzare il nuovo campo senza interrompere le applicazioni esistenti. La possibilità di estendere le classi base di una gerarchia di classi è un importante vantaggio che l'utilizzo delle interfacce non offre.
Vedere anche
Concetti
Quando utilizzare le interfacce
Modifica della progettazione delle classi base dopo la distribuzione