Quand utiliser l'héritage
Mise à jour : novembre 2007
L'héritage est un concept de programmation utile, mais pouvant être utilisé de façon incorrecte. Les interfaces effectuent souvent mieux cette opération. Cette rubrique et la rubrique Quand utiliser des interfaces vous permettent de mieux déterminer les situations dans lesquelles il convient d'utiliser ces approches.
L'héritage est approprié dans les situations suivantes :
Votre hiérarchie d'héritage est une relation de type « être » et non une relation de type « avoir ».
Vous pouvez réutiliser du code des classes de base.
Vous devez appliquer la même classe et les mêmes méthodes à divers types de données.
La hiérarchie de classe est assez superficielle et il est peu probable que d'autres développeurs ajoutent des niveaux supplémentaires.
Vous souhaitez apporter des modifications globales à des classes dérivées en modifiant une classe de base.
Ces différents aspects sont décrits, dans l'ordre, ci-dessous.
Héritage et relations de type « être »
Il existe deux façons de présenter les relations de classe dans la programmation orientée objet : les relations de type « être » et « avoir ». Dans une relation de type « être », la classe dérivée est un type de la classe de base. Par exemple, une classe intitulée PremierCustomer représente une relation de type « être » dont la classe de base est Customer, car un premier client est un client. Toutefois, une classe intitulée CustomerReferral représente une relation de type « avoir » avec la classe Customer car une référence de client appartient à un client, mais n'est pas un type de client.
Les objets d'une hiérarchie d'héritage doivent avoir une relation de type « être » avec leur classe de base car ils héritent des champs, des propriétés, des méthodes et des événements définis dans la classe de base. Les classes qui représentent une relation de type « avoir » avec d'autres classes ne conviennent pas aux hiérarchies d'héritage car elles peuvent hériter de propriétés et de méthodes inadaptées. Par exemple, si la classe CustomerReferral est dérivée de la classe Customer évoquée précédemment, elle peut hériter de propriétés absurdes, telles que ShippingPrefs et LastOrderPlaced. Une telle relation de type « avoir » doit être représentée en utilisant des classes ou des interfaces non apparentées. L'illustration suivante montre des exemples des relations « est un » et « a un ».
Classes de base et réutilisation de code
L'utilisation de l'héritage présente l'avantage de pouvoir réutiliser du code. Les classes bien conçues peuvent être déboguées une seule fois et réutilisées comme base des nouvelles classes.
Un exemple courant d'une réutilisation efficace du code concerne les bibliothèques qui gèrent des structures de données. Supposons par exemple que vous ayez une grande application d'entreprise qui gère plusieurs types de listes en mémoire. L'une de ces listes est une copie en mémoire de votre base de données des clients, lue depuis une base de données située au début de la session pour augmenter la vitesse. La structure de données peut se présenter comme suit :
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
Votre application peut également posséder une liste similaire des produits ajoutés par un utilisateur à son panier d'achat, comme l'illustre le fragment de code suivant :
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
En voici un modèle : deux listes se comportent de la même façon (insertions, suppressions et extractions), mais fonctionnent sur divers types de données. La gestion de deux bases de code pour effectuer essentiellement les mêmes fonctions n'est pas efficace. La solution la plus efficace consiste à isoler la gestion de la liste dans sa propre classe et d'hériter ensuite de cette classe pour divers types de données :
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
La classe ListItem ne doit être déboguée qu'une seule fois. Vous pouvez ensuite construire des classes qui l'utilisent sans devoir vous préoccuper de la gestion de la liste. Par exemple :
Class CustomerInfo
Inherits ListItem
Public ID As Integer
Public FullName As String
End Class
Class ShoppingCartItem
Inherits ListItem
Public ProductCode As Integer
End Class
Bien que la réutilisation du code d'héritage soit un outil puissant, elle présente également certains risques. Même les systèmes les mieux conçus peuvent varier d'une façon dont les concepteurs n'avaient pas prévu. Les modifications d'une hiérarchie de classe existante peuvent parfois entraîner des conséquences inattendues. Pour plus d'informations, consultez la section « Problème de classe de base fragile » sous la rubrique Modifications du design de la classe de base après le déploiement.
Classes dérivées interchangeables
Les classes dérivées d'une hiérarchie de classe peuvent parfois être utilisées de façon interchangeable avec leur classe de base. Ce processus est appelé le polymorphisme d'héritage. Cette approche associe les meilleures fonctionnalités du polymorphisme d'interface avec l'option permettant de réutiliser ou de substituer du code d'une classe de base.
Un package de dessin représente un exemple où cette méthode pourrait s'avérer utile. Par exemple, examinons le fragment de code suivant qui n'utilise pas l'héritage :
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
Cette approche présente plusieurs problèmes. Si quelqu'un décide d'ajouter ultérieurement une option d'ellipse, le code source devra être modifié ; dans ce cas, il est possible que les utilisateurs cibles ne puissent plus accéder à votre code source. Un problème plus subtil est le suivant : pour dessiner une ellipse, vous avez besoin d'un autre paramètre (les ellipses ont un diamètre majeur et mineur) qui n'est pas adapté aux lignes. Si quelqu'un veut ensuite ajouter une polyligne (plusieurs lignes reliées), un autre paramètre doit être ajouté qui ne correspondrait pas aux autres cas.
L'héritage permet de résoudre la plupart de ces problèmes. Les classes de base bien conçues laissent l'implémentation de méthodes spécifiques aux classes dérivées, de sorte qu'il est possible d'accommoder tout type de forme. D'autres développeurs peuvent implémenter des méthodes dans des classes dérivées en utilisant la documentation de la classe de base. D'autres éléments de classe (tels que les coordonnées x et y) peuvent être construits dans la classe de base car tous les descendants les utilisent. Par exemple, Draw peut être une méthode MustOverride :
MustInherit Class Shape
Public X As Integer
Public Y As Integer
MustOverride Sub Draw()
End Class
Vous pouvez ensuite ajouter des paramètres à cette classe pour diverses formes. Par exemple, il est possible qu'une classe Line ne nécessite qu'un champ 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
Cette approche est utile car les autres développeurs, qui ne peuvent pas accéder à votre code source, peuvent étendre leur classe de base avec de nouvelles classes dérivées si nécessaire. Par exemple, une classe intitulée Rectangle peut être dérivée de la classe Line :
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
Cet exemple illustre la façon dont vous pouvez passer des classes générales à des classes très spécifiques en ajoutant des détails d'implémentation à chaque niveau.
À ce niveau, il est recommandé de réévaluer si la classe dérivée représente réellement une relation de type « être » ou s'il s'agit au contraire d'une relation de type « avoir ». Si la nouvelle classe rectangle se compose uniquement de lignes, l'héritage ne représente pas la méthode la mieux adaptée. Toutefois, si le nouveau rectangle est une ligne comprenant une propriété de largeur, la relation de type « être » est conservée.
Hiérarchies de classes superficielles
L'héritage est recommandé pour les hiérarchies de classes assez superficielles. Les hiérarchies de classe très profondes et complexes peuvent être difficiles à développer. La décision d'utiliser une hiérarchie de classe implique une analyse des avantages liés à l'utilisation d'une hiérarchie de classe par rapport à la complexité. En règle générale, vous devez limiter les hiérarchies à six niveaux maximum. Toutefois, la profondeur maximale d'une hiérarchie de classe particulière dépend d'un nombre de facteurs, y compris de la quantité de complexité à chaque niveau.
Modifications globales des classes dérivées par l'intermédiaire de la classe de base
L'une des fonctionnalités les plus puissantes de l'héritage est la possibilité d'apporter des modifications dans une classe de base pour les répercuter dans les classes dérivées. Si vous procédez avec prudence, il suffit de mettre à jour l'implémentation d'une seule méthode pour que des douzaines, voire des centaines de classes dérivées puissent utiliser le nouveau code. Toutefois, cette opération peut être dangereuse car de telles modifications peuvent engendrer des problèmes avec les classes héritées conçues par de tierces personnes. Vous devez être prudent pour garantir que la nouvelle classe de base est compatible avec les classes qui utilisent la classe d'origine. Vous devez particulièrement éviter de modifier le nom ou le type des membres de la classe de base.
Supposons par exemple que vous conceviez une classe de base avec un champ de type Integer pour stocker des informations concernant le code postal et que d'autres développeurs aient créé des classes dérivées qui utilisent le champ de code postal hérité. Supposons également que votre champ de code postal puisse stocker cinq chiffres, mais que le bureau de poste ait modifié les codes postaux en leur ajoutant un tiret et quatre chiffres supplémentaires. Dans le pire scénario, vous pouvez modifier le champ de la classe de base pour stocker une chaîne de 10 caractères, mais les autres développeurs doivent modifier et recompiler les classes dérivées afin d'utiliser la nouvelle taille et le nouveau type de données.
Le moyen le plus sûr pour modifier une classe de base est de lui ajouter de nouveaux membres. Par exemple, vous pouvez ajouter un nouveau champ pour stocker les quatre chiffres supplémentaires du code postal de l'exemple précédent. Les applications clientes peuvent ainsi être mises à jour pour utiliser le nouveau champ sans endommager les applications existantes. La possibilité d'étendre les classes de base dans une hiérarchie d'héritage est un avantage important qui n'existe pas avec les interfaces.
Voir aussi
Concepts
Modifications du design de la classe de base après le déploiement