Si tout va bien pas trop académique ...
Disons que j'ai besoin de nombres réels et complexes dans ma bibliothèque SW.
Basé sur la relation is-a (ou ici ), le nombre réel est un nombre complexe , où b dans la partie imaginaire d'un nombre complexe est simplement 0.
D'un autre côté, mon implémentation serait que cet enfant étend le parent, donc dans le parent RealNumber, j'aurais une partie réelle et l'enfant ComplexNumber ajouterait de l'art imaginaire.
Il y a aussi une opinion, que l'héritage est mauvais .
Je me souviens comme hier, quand j'apprenais OOP à l'université, mon professeur a dit, ce n'est pas un bon exemple d'héritage car la valeur absolue de ces deux est calculée différemment (mais pour cela nous avons la méthode surcharge/polymorfisme, non?) ...
Mon expérience est que nous utilisons souvent l'héritage pour résoudre DRY, par conséquent, nous avons souvent des classes abstraites artificielles dans la hiérarchie (nous avons souvent du mal à trouver des noms car ils ne représentent pas des objets d'un monde réel).
Même si dans un sens mathématique, un nombre réel est un nombre complexe, ce n'est pas une bonne idée de dériver le réel du complexe. Il viole le principe de substitution de Liskov en disant (entre autres) qu'une classe dérivée ne doit pas cacher les propriétés d'une classe de base.
Dans ce cas, un nombre réel devrait masquer la partie imaginaire du nombre complexe. Il est clair que cela n'a aucun sens de stocker un nombre à virgule flottante caché (partie imaginaire) si vous n'avez besoin que de la partie réelle.
Il s'agit essentiellement du même problème que l'exemple rectangle/carré mentionné dans un commentaire.
pas un bon exemple d'héritage car la valeur absolue de ces deux est calculée différemment
Ce n'est en fait pas une raison convaincante contre l'héritage tous ici, juste le modèle class RealNumber
<-> class ComplexNumber
Proposé.
Vous pouvez raisonnablement définir une interface Number
, que les deuxRealNumber
et ComplexNumber
implémenterait.
Cela pourrait ressembler à
interface Number
{
Number Add(Number rhs);
Number Subtract(Number rhs);
// ... etc
}
Mais vous voudriez alors contraindre les autres paramètres Number
dans ces opérations à être du même type dérivé que this
, que vous pouvez approcher avec
interface Number<T>
{
Number<T> Add(Number<T> rhs);
Number<T> Subtract(Number<T> rhs);
// ... etc
}
Ou à la place, vous utiliseriez un langage qui permettait le polymorphisme structurel, au lieu du polymorphisme de sous-type. Pour le cas spécifique de nombres, il se peut que vous n'ayez besoin que de surcharger les opérateurs arithmétiques.
complex operator + (complex lhs, complex rhs);
complex operator - (complex lhs, complex rhs);
// ... etc
Number frobnicate<Number>(List<Number> foos, Number bar); // uses arithmetic operations
RealNumber
Je trouverais tout à fait correct si ComplexNumber
avait une méthode d'usine statique fromDouble(double)
qui retournerait un nombre complexe avec un imaginaire égal à zéro. Vous pouvez ensuite utiliser toutes les opérations que vous utiliseriez sur une instance RealNumber
sur cette instance ComplexNumber
.
Mais j'ai du mal à comprendre pourquoi vous souhaitez/devez avoir une classe RealNumber
publique héritée. L'héritage est généralement utilisé pour ces raisons (hors de ma tête, corrigez-moi si vous en avez manqué)
étendre le comportement. RealNumbers
ne peut pas faire d'opérations supplémentaires que le nombre complexe ne peut pas faire, donc inutile de le faire.
implémenter un comportement abstrait avec une implémentation spécifique. Puisque ComplexNumber
ne doit pas être abstrait, cela ne s'applique pas non plus.
réutilisation du code. Si vous utilisez simplement la classe ComplexNumber
, vous réutilisez 100% du code.
mise en œuvre plus spécifique/efficace/précise pour une tâche spécifique. Cela pourrait être appliqué ici, RealNumbers
pourrait implémenter certaines fonctionnalités plus rapidement. Mais alors cette sous-classe doit être cachée derrière le fromDouble(double)
statique et ne doit pas être connue à l'extérieur. De cette façon, il n'aurait pas besoin de cacher la partie imaginaire. Pour l'extérieur, il ne devrait y avoir que des nombres complexes (quels sont les nombres réels). Vous pouvez également renvoyer cette classe RealNumber privée à partir de toutes les opérations de la classe de nombres complexes qui aboutissent à un nombre réel. (Cela suppose que les classes sont immuables comme la plupart des classes numériques.)
C'est comme implémenter une sous-classe d'Integer qui s'appelle Zero et coder en dur certaines des opérations car elles sont triviales pour zéro. Vous pouvez le faire, car chaque zéro est un entier, mais ne le rendez pas public, cachez-le derrière une méthode d'usine.
Dire qu'un nombre réel est un nombre complexe a plus de sens en mathématiques, en particulier en théorie des ensembles, qu'en informatique.
En mathématiques, nous disons:
Cependant, cela ne signifie pas que vous devez, ou même devez, utiliser l'héritage lors de la conception de votre bibliothèque pour inclure une classe RealNumber et ComplexNumber. In Effective Java, Second Edition par Joshua Bloch; Le point 16 est "Favoriser la composition plutôt que l'héritage". Pour éviter les problèmes mentionnés dans cet élément, une fois que vous avez défini votre classe RealNumber, elle peut être utilisée dans votre classe ComplexNumber:
public class ComplexNumber {
private RealNumber realPart;
private RealNumber imaginaryPart;
// Implementation details are for you to write
}
Cela vous donne toute la puissance de réutiliser votre classe RealNumber pour conserver votre code DRY tout en évitant les problèmes identifiés par Joshua Bloch.
Il y a deux problèmes ici. La première est qu'il est courant d'utiliser les mêmes termes pour les types de conteneurs et les types de leur contenu, en particulier avec les types primitifs comme les nombres. Le terme double
, par exemple, est utilisé pour décrire à la fois une valeur à virgule flottante double précision et un conteneur dans lequel une valeur peut être stockée.
Le deuxième problème est que si les relations is-a entre les conteneurs à partir desquels différents types d'objets peuvent être lus se comportent de la même manière que les relations entre les objets eux-mêmes, celles entre les conteneurs dans lesquels différents types d'objets peuvent être placés se comportent en face de celles de leur contenu. . Chaque cage connue pour contenir une instance de Cat
sera une cage qui contient une instance de Animal
, mais ne doit pas nécessairement être une cage qui contient une instance de SiameseCat
. D'un autre côté, chaque cage pouvant contenir toutes les instances de Cat
sera une cage pouvant contenir toutes les instances de SiameseCat
, mais il n'est pas nécessaire qu'elle soit une cage pouvant contenir toutes les instances de Animal
. Le seul type de cage pouvant contenir toutes les instances de Cat
et pouvant être garanti ne contient jamais autre chose qu'une instance de Cat
, est une cage de Cat
. Tout autre type de cage serait soit incapable d'accepter certaines instances de Cat
qu'il devrait accepter, soit serait capable d'accepter des choses qui ne sont pas des instances de Cat
.