Dans un de mes projets récents, j'ai défini une classe avec l'en-tête suivant:
public class Node extends ArrayList<Node> {
...
}
Cependant, après avoir discuté avec mon professeur d’informatique, il a déclaré que le cours serait à la fois "horrible pour la mémoire" et "une mauvaise pratique". Je n'ai pas trouvé le premier particulièrement véridique et le second subjectif.
Mon raisonnement pour cet usage est que j'ai eu l'idée d'un objet qui devait être défini comme quelque chose pouvant avoir une profondeur arbitraire, où le comportement d'une instance pourrait être défini soit par une implémentation personnalisée, soit par le comportement de plusieurs objets similaires en interaction. . Cela permettrait d'abstraction d'objets dont l'implémentation physique serait composée de nombreuses sous-composantes en interaction¹.
D'autre part, je vois à quel point cela pourrait être une mauvaise pratique. L’idée de définir quelque chose comme une liste d’elle-même n’est ni simple ni physiquement applicable.
Existe-t-il une raison valable pour ne pas utiliser cela dans mon code, compte tenu de mon utilisation?
¹ Si j’ai besoin d’expliquer plus avant, j’en serais ravi; J'essaie simplement de garder cette question concise.
la source
ArrayList<Node>
, par rapport à mettre en œuvreList<Node>
?Réponses:
Franchement, je ne vois pas la nécessité d'un héritage ici. Ça n'a pas de sens;
Node
est unArrayList
deNode
?S'il ne s'agit que d'une structure de données récursive, vous écrirez simplement quelque chose comme:
Ce qui a du sens noeud a une liste d'enfants ou de noeuds descendants.
la source
Item
etItem
fonctionne indépendamment des listes.Node
est unArrayList
desNode
commeNode
contient 0 à de nombreuxNode
objets. Leur fonction est essentiellement la même, mais au lieu d’ être une liste d’objets similaires, elle contient une liste d’objets similaires. La conception initiale de la classe écrite était la conviction que toutNode
peut être exprimé sous la forme d'un ensemble de sous-Node
titres, ce que les deux implémentations font, mais celui-ci est certainement moins déroutant. +1Children
une ou deux fois, et il est généralement préférable de l'écrire dans de tels cas pour des raisons de clarté du code.L'argument «horrible pour la mémoire» est totalement faux, mais c'est objectivement une «mauvaise pratique». Lorsque vous héritez d'une classe, vous n'héritez pas seulement des champs et des méthodes qui vous intéressent. Au lieu de cela, vous héritez de tout . Chaque méthode qu’elle déclare, même si elle ne vous est pas utile. Et surtout, vous héritez également de tous ses contrats et garanties que la classe fournit.
L'acronyme SOLID fournit des heuristiques pour une bonne conception orientée objet. Ici, le I nterface Principe Ségrégation (ISP de) et le L Iskov Remplacement Pricinple (LSP) ont quelque chose à dire.
Le FAI nous dit de garder nos interfaces aussi petites que possible. Mais en héritant de
ArrayList
, vous obtenez beaucoup, beaucoup de méthodes. Est - il un sensget()
,remove()
,set()
(remplacer), ouadd()
(insérer) un nœud enfant à un index particulier? Est-il judicieux deensureCapacity()
de la liste sous-jacente? Qu'est-ce que cela signifie poursort()
un nœud? Les utilisateurs de votre classe sont-ils vraiment supposés en avoirsubList()
? Étant donné que vous ne pouvez pas masquer les méthodes que vous ne voulez pas, la seule solution est d'avoir ArrayList en tant que variable membre et de transférer toutes les méthodes que vous voulez réellement:Si vous voulez vraiment toutes les méthodes que vous voyez dans la documentation, nous pouvons passer au LSP. Le LSP nous dit que nous devons pouvoir utiliser la sous-classe partout où la classe parente est attendue. Si une fonction prend un
ArrayList
comme paramètre et que nous passons le nôtreNode
, rien n’est supposé changer.La compatibilité des sous-classes commence par des éléments simples tels que les signatures de type. Lorsque vous substituez une méthode, vous ne pouvez pas rendre les types de paramètre plus stricts, car cela pourrait exclure les utilisations légales de la classe parent. Mais c’est quelque chose que le compilateur vérifie pour nous en Java.
Mais le LSP est beaucoup plus profond: nous devons maintenir la compatibilité avec tout ce qui est promis par la documentation de toutes les classes et interfaces parent. Dans leur réponse , Lynn a trouvé un cas de ce type où l'
List
interface (dont vous avez hérité viaArrayList
) garantit le fonctionnement des méthodesequals()
ethashCode()
. CarhashCode()
on vous donne même un algorithme particulier qui doit être implémenté exactement. Supposons que vous avez écrit ceciNode
:Cela nécessite que le
value
ne puisse ni contribuerhashCode()
ni influencerequals()
. L'List
interface - que vous promettez d'honorer en en héritant - nécessitenew Node(0).equals(new Node(123))
d'être vraie.En raison de l'héritage de classes, il est trop facile de rompre accidentellement une promesse faite par une classe parente, et parce qu'elle expose généralement plus de méthodes que prévu, il est généralement suggéré de préférer la composition à l'héritage . Si vous devez hériter de quelque chose, il est suggéré de n'hériter que des interfaces. Si vous souhaitez réutiliser le comportement d'une classe particulière, vous pouvez la conserver en tant qu'objet séparé dans une variable d'instance, afin que toutes ses promesses et exigences ne vous soient pas imposées.
Parfois, notre langage naturel suggère une relation d'héritage: une voiture est un véhicule. Une moto est un véhicule. Devrais-je définir des classes
Car
etMotorcycle
qui héritent d'uneVehicle
classe? La conception orientée objet ne consiste pas à refléter exactement le monde réel dans notre code. Nous ne pouvons pas facilement encoder les riches taxonomies du monde réel dans notre code source.Un exemple de ce type est le problème de modélisation employé-patron. Nous avons plusieurs
Person
s, chacun avec un nom et une adresse. UnEmployee
est unPerson
et a unBoss
. ABoss
est aussi unPerson
. Donc, devrais-je créer unePerson
classe qui est héritée parBoss
etEmployee
? Maintenant, j'ai un problème: le patron est aussi un employé et a un autre supérieur. Donc, il semble queBoss
devrait s'étendreEmployee
. MaisCompanyOwner
c'est unBoss
mais n'est pas unEmployee
? Tout type de graphique d'héritage se décompose d'une manière ou d'une autre ici.La POO ne concerne pas les hiérarchies, l'héritage et la réutilisation de classes existantes, il s'agit de généraliser le comportement . OOP signifie «J'ai plusieurs objets et je veux qu'ils fassent quelque chose - et je me fiche de savoir comment.» C'est à cela que servent les interfaces . Si vous implémentez l'
Iterable
interface pour vousNode
parce que vous voulez la rendre itérable, c'est très bien. Si vous implémentez l'Collection
interface parce que vous souhaitez ajouter / supprimer des nœuds enfants, etc., c'est très bien. Mais hériter d'une autre classe parce qu'il arrive à vous donner tout ce qui ne l'est pas, ou du moins pas à moins d'y avoir bien réfléchi.la source
L'extension d'un conteneur en soi est généralement considérée comme une mauvaise pratique. Il y a vraiment très peu de raisons d'étendre un conteneur au lieu d'en avoir un. L'extension d'un conteneur de vous-même le rend encore plus étrange.
la source
En plus de ce qui a été dit, il existe une raison un peu spécifique à Java pour éviter ce type de structure.
Le contrat de la
equals
méthode pour les listes nécessite qu'une liste soit considérée comme égale à un autre objetSource: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
Cela signifie notamment que le fait de concevoir une classe comme une liste en soi peut rendre les comparaisons d'égalité coûteuses (ainsi que les calculs de hachage si les listes sont modifiables), et si la classe contient des champs d'instance, ceux-ci doivent être ignorés dans la comparaison d'égalité. .
la source
En ce qui concerne la mémoire:
Je dirais que c'est une question de perfectionnisme. Le constructeur par défaut de
ArrayList
ressemble à ceci:Source . Ce constructeur est également utilisé dans Oracle-JDK.
Maintenant, envisagez de construire une liste à lien unique avec votre code. Vous venez de gonfler votre consommation de mémoire du facteur 10x (pour être précis, même légèrement supérieur). Pour les arbres, cela peut facilement devenir assez mauvais sans aucune exigence particulière sur la structure de l'arbre. Utiliser
LinkedList
alternativement une classe ou une autre devrait résoudre ce problème.Pour être honnête, dans la plupart des cas, il ne s'agit que de perfectionnisme, même si nous ignorons la quantité de mémoire disponible.
LinkedList
Comme alternative, A ralentirait un peu le code, ce serait donc un compromis entre les performances et la consommation de mémoire. Pourtant, mon opinion personnelle à ce sujet serait de ne pas gaspiller autant de mémoire d’une manière qui puisse être aussi facilement contournée que celle-ci.EDIT: clarification concernant les commentaires (@amon pour être précis). Cette section de la réponse ne traite pas de la question de l'héritage. La comparaison de l'utilisation de la mémoire est effectuée par rapport à une liste liée de manière simple et avec une utilisation optimale de la mémoire (dans les implémentations réelles, le facteur peut changer un peu, mais il reste suffisamment important pour résumer un peu de mémoire gaspillée).
En ce qui concerne les "mauvaises pratiques":
Absolument. Ce n’est pas la manière standard d’implémenter un graphique pour une raison simple: un graphe-nœud a des nœuds enfants, pas une liste de nœuds enfants. Exprimer précisément ce que vous voulez dire dans le code est une compétence majeure. Que ce soit dans des noms de variable ou des structures d'expression telles que celle-ci. Point suivant: minimiser les interfaces: vous avez mis toutes les méthodes de à la
ArrayList
disposition de l'utilisateur de la classe via l'héritage. Il n'y a aucun moyen de modifier cela sans casser toute la structure du code. Stockez plutôt laList
variable en tant que variable interne et rendez les méthodes requises disponibles via une méthode adaptateur. De cette façon, vous pouvez facilement ajouter et supprimer des fonctionnalités de la classe sans tout gâcher.la source
new Object[0]
utilise 4 mots au total, unnew Object[10]
n’utiliserait que 14 mots de mémoire.ArrayList
perdent un peu de mémoire.Cela semble ressembler à la sémantique du motif composite . Il est utilisé pour modéliser des infrastructures de données récursives.
la source
public class Node extends ArrayList<Node> { public string Item; }
est la version d'héritage de votre réponse. Il est plus simple de raisonner, mais vous réussissez tous les deux: ils définissent des arbres non binaires.