De: http://www.artima.com/lejava/articles/designprinciples4.html
Erich Gamma: Je pense toujours que c'est vrai même après dix ans. L'héritage est un moyen intéressant de changer de comportement. Mais nous savons qu’elle est fragile, car la sous-classe peut facilement émettre des hypothèses sur le contexte dans lequel une méthode qu’elle substitue est appelée. Il existe un couplage étroit entre la classe de base et la sous-classe, en raison du contexte implicite dans lequel le code de sous-classe que je connecte sera appelé. La composition a une propriété plus agréable. Le couplage est réduit simplement en ayant quelques petites choses que vous branchez dans quelque chose de plus grand, et le plus gros objet rappelle simplement le plus petit objet. Du point de vue de l'API, définir qu'une méthode peut être remplacée est un engagement plus important que de définir qu'une méthode peut être appelée.
Je ne comprends pas ce qu'il veut dire. Quelqu'un pourrait-il s'il vous plaît l'expliquer?
Si vous publiez une fonction normale, vous donnez un contrat unilatéral:
que fait la fonction si elle est appelée?
Si vous publiez un rappel, vous accordez également un contrat unilatéral:
quand et comment sera-t-il appelé?
Et si vous publiez une fonction qui peut être annulée, ce sont les deux à la fois, vous donnez donc un contrat à double face:
quand sera-t-elle appelée et que doit-elle faire si elle est appelée?
Même si vos utilisateurs n'abusent pas de votre API (en rompant leur part du contrat, ce qui pourrait coûter très cher à détecter), vous pouvez facilement voir que cette dernière a besoin de beaucoup plus de documentation, et tout ce que vous documentez est un engagement qui limite vos autres choix.
Un exemple de revenir sur un tel contrat bilatéral est le passage de
show
ethide
àsetVisible(boolean)
en java.awt.Component .la source
show
ethide
existent toujours, ils sont juste@Deprecated
. Ainsi, le changement ne casse aucun code qui les invoque simplement. Mais si vous les avez remplacées, vos remplacements ne seront pas appelés par les clients qui migrent vers le nouveau "setVisible". (Je n'ai jamais utilisé Swing, donc je ne sais pas à quel point il est courant de passer outre cela; mais depuis que cela s'est passé il y a longtemps, j'imagine que la raison pour laquelle Deduplicator se souvient c'est que c'est lui qui l'a mordu douloureusement.)La réponse de Kilian Foth est excellente. J'aimerais juste ajouter l'exemple canonique * de la raison pour laquelle c'est un problème. Imaginez une classe de points entière:
Maintenant, passons à une classe 3D.
Super simple! Utilisons nos points:
Vous vous demandez probablement pourquoi je publie un exemple aussi simple. Voici le piège:
Lorsque nous comparons le point 2D au point 3D équivalent, nous obtenons la valeur true, mais lorsque nous inversons la comparaison, nous obtenons la valeur false (car p2a échoue
instanceof Point3D
).Conclusion
Il est généralement possible d'implémenter une méthode dans une sous-classe de telle sorte qu'elle ne soit plus compatible avec la façon dont la super-classe s'attend à ce qu'elle fonctionne.
Il est généralement impossible d'implémenter equals () sur une sous-classe très différente d'une manière compatible avec sa classe parente.
Lorsque vous écrivez un cours que vous avez l'intention de permettre aux gens de sous-classer, c'est une très bonne idée d'écrire un contrat. précisant le comportement de chaque méthode. Mieux encore, un ensemble de tests unitaires que les personnes pourraient exécuter contre leur implémentation de méthodes annulées pour prouver qu’elles ne violaient pas le contrat. Presque personne ne le fait parce que c'est trop de travail. Mais si vous vous en souciez, c'est la chose à faire.
Comparateur est un excellent exemple de contrat bien défini . Ignorez simplement ce qui est dit
.equals()
pour les raisons décrites ci-dessus. Voici un exemple de ce que le comparateur ne.equals()
peut pas faire .Remarques
L'élément 8 de "Effective Java" de Josh Bloch était la source de cet exemple, mais Bloch utilise un ColorPoint qui ajoute une couleur au lieu d'un troisième axe et utilise des doubles au lieu d'ints. L'exemple Java de Bloch est essentiellement dupliqué par Odersky / Spoon / Venners qui a rendu leur exemple disponible en ligne.
Plusieurs personnes se sont opposées à cet exemple car si vous informez la classe parent de la sous-classe, vous pouvez résoudre ce problème. Cela est vrai s’il ya un nombre de sous-classes assez petit et si le parent les connaît toutes. Mais la question initiale concernait la création d'une API pour laquelle quelqu'un d'autre écrirait des sous-classes. Dans ce cas, vous ne pouvez généralement pas mettre à jour l'implémentation parent pour qu'elle soit compatible avec les sous-classes.
Prime
Comparator est également intéressant car il résout correctement le problème de la mise en œuvre de equals () Mieux encore, un modèle permet de résoudre ce type de problème d'héritage: le modèle de conception Stratégie. Les classes qui intéressent les gens de Haskell et de Scala constituent également le modèle de la stratégie. L'héritage n'est ni mauvais ni faux, c'est simplement délicat. Pour en savoir plus, consultez l'article de Philip Wadler Comment rendre le polymorphisme ad hoc moins ad hoc
la source
equals
façon dont Map et Set le définissent. Equality ignore complètement l'ordre, avec pour conséquence que, par exemple, deux SortedSets avec les mêmes éléments mais des ordres de tri différents se comparent toujours de manière égale.B
est un enfant d'une classeA
et que vous instanciez un objet d'une classeB
, vous devriez pouvoir transtyper l'B
objet de classe sur son parent et utiliser l'API de la variable convertie sans perdre les détails d'implémentation de l'enfant. Vous avez enfreint la règle en fournissant la troisième propriété. Comment comptez-vous accéder auxz
coordonnées après avoir converti laPoint3D
variable enPoint2D
, alors que la classe de base n'a aucune idée de l'existence d'une telle propriété? Si en cassant une classe enfant vers sa base vous cassez l'API publique, votre abstraction est fausse.L'héritage affaiblit l'encapsulation
Lorsque vous publiez une interface avec l'héritage autorisé, vous augmentez considérablement la taille de votre interface. Chaque méthode pouvant être remplacée peut être remplacée et doit donc être considérée comme un rappel fourni au constructeur. L'implémentation fournie par votre classe est simplement la valeur par défaut du rappel. Il convient donc de fournir un type de contrat indiquant les attentes vis-à-vis de la méthode. Cela se produit rarement et est l'une des principales raisons pour lesquelles le code orienté objet est appelé fragile.
Vous trouverez ci-dessous un exemple réel (simplifié) du framework de collections Java, gracieuseté de Peter Norvig ( http://norvig.com/java-iaq.html ).
Alors que se passe-t-il si nous sous-classe cela?
Nous avons un bug: Nous ajoutons parfois "dog" et la hashtable reçoit une entrée pour "dogss". La cause était une personne fournissant une implémentation de put à laquelle la personne qui conçoit la classe Hashtable ne s’attendait pas.
L'héritage rompt l'extensibilité
Si vous autorisez la sous-classe de votre classe, vous vous engagez à ne pas ajouter de méthodes à votre classe. Cela pourrait autrement être fait sans rien casser.
Lorsque vous ajoutez de nouvelles méthodes à une interface, toute personne ayant hérité de votre classe devra implémenter ces méthodes.
la source
Si une méthode doit être appelée, vous devez simplement vous assurer qu'elle fonctionne correctement. C'est ça. Terminé.
Si une méthode est conçue pour être remplacée, vous devez également bien réfléchir à la portée de la méthode: si la portée est trop grande, la classe enfant devra souvent inclure le code copié-collé de la méthode parente; si elle est trop petite, de nombreuses méthodes devront être remplacées pour que les nouvelles fonctionnalités souhaitées soient ajoutées, ce qui ajoute à la complexité et au nombre de lignes inutile.
Par conséquent, le créateur de la méthode parent doit émettre des hypothèses sur la manière dont la classe et ses méthodes pourraient être remplacées à l'avenir.
Cependant, l'auteur parle d'un problème différent dans le texte cité:
Considérez la méthode
a
qui est normalement appelée méthodeb
, mais dans certains cas rares et non évidents de méthodec
. Si l'auteur de la méthode dominante néglige lac
méthode et ses attentesa
, il est évident que les choses peuvent mal se passer.Par conséquent, il est plus important de
a
définir clairement et sans ambiguïté, bien documenté, "fait une chose et le fait bien" - davantage que s'il s'agissait d'une méthode conçue uniquement pour être appelée.la source