Je préfère les fonctionnalités de POO lors du développement de jeux avec Unity. Je crée généralement une classe de base (principalement abstraite) et utilise l'héritage d'objet pour partager les mêmes fonctionnalités avec les autres objets.
Cependant, j'ai récemment entendu quelqu'un dire qu'il fallait éviter l'utilisation de l'héritage et utiliser plutôt des interfaces. Alors j'ai demandé pourquoi, et il a dit que "l'héritage d'objet est important bien sûr, mais s'il y a beaucoup d'objets étendus, les relations entre les classes sont profondément couplées.
J'utilise une classe de base abstraite, quelque chose comme WeaponBase
et la création de classes d'armes spécifiques comme Shotgun
, AssaultRifle
, Machinegun
quelque chose comme ça. Il y a tellement d'avantages et l'un des motifs que j'apprécie vraiment est le polymorphisme. Je peux traiter l'objet créé par les sous-classes comme s'il s'agissait de la classe de base, de sorte que la logique puisse être considérablement réduite et rendue plus réutilisable. Si j'ai besoin d'accéder aux champs ou aux méthodes de la sous-classe, je renvoie à la sous-classe.
Je ne veux pas définir mêmes champs, couramment utilisés dans les différentes classes, comme AudioSource
/ Rigidbody
/ Animator
et beaucoup de champs membres comme je définis fireRate
, damage
, range
, spread
et ainsi de suite. De plus, dans certains cas, certaines méthodes pourraient être écrasées. J'utilise donc des méthodes virtuelles dans ce cas, afin de pouvoir invoquer cette méthode à l'aide de la méthode de la classe parente, mais si la logique est différente dans child, je les ai remplacées manuellement.
Alors, faut-il que toutes ces choses soient abandonnées en tant que mauvaises pratiques? Devrais-je utiliser des interfaces à la place?
la source
Réponses:
Privilégiez la composition à l'héritage dans vos systèmes d'entité et d'inventaire / article. Ce conseil s’applique généralement aux structures de logique de jeu, lorsque la manière dont vous pouvez assembler des choses complexes (au moment de l’exécution) peut conduire à de nombreuses combinaisons différentes; c'est quand on préfère la composition.
Privilégiez l'héritage à la composition dans votre logique au niveau de l'application, des constructions d'interface utilisateur aux services. Par exemple,
Widget->Button->SpinnerButton
ouConnectionManager->TCPConnectionManager
vs->UDPConnectionManager
.... il existe ici une hiérarchie de dérivation clairement définie, plutôt qu'une multitude de dérivations potentielles, de sorte qu'il est simplement plus facile d'utiliser l'héritage.
En bout de ligne : utilisez l'héritage où vous pouvez, mais utilisez la composition où vous le devez. PS L'autre raison pour laquelle nous pouvons favoriser la composition dans les systèmes d'entité est qu'il existe généralement de nombreuses entités et que l'héritage peut entraîner un coût de performance pour accéder aux membres de chaque objet. voir vtables .
la source
P.S. The other reason we may favour composition in entity systems is ... performance
: Est-ce vraiment vrai? Si vous regardez la page WIkipedia liée par @Linaith, vous devez composer votre objet d’interfaces. Par conséquent, vous avez (encore ou même davantage) d'appels de fonction virtuels et plus de cache manquants, car vous avez introduit un autre niveau d'indirection.Vous avez déjà quelques bonnes réponses, mais l’énorme éléphant dans la pièce de votre question est celui-ci:
En règle générale, lorsque quelqu'un vous donne une règle empirique, alors ignorez-la. Cela vaut non seulement pour "quelqu'un qui vous dit quelque chose", mais aussi pour lire des choses sur Internet. À moins que vous ne sachiez pourquoi (et que vous puissiez vraiment le supporter), un tel conseil est sans valeur et souvent très dangereux.
D'après mon expérience, les concepts les plus importants et les plus utiles de la programmation orientée objet sont "faible couplage" et "forte cohésion" (les classes / objets se connaissent le moins possible les uns sur les autres et chaque unité est responsable du moins de choses possible).
Faible couplage
Cela signifie que tout "paquet" de votre code doit dépendre le moins possible de son environnement. Cela vaut pour les classes (conception de classes) mais aussi pour les objets (implémentation réelle), les "fichiers" en général (c'est-à-dire le nombre de
#include
s par.cpp
fichier, le nombre de fichiersimport
par.java
fichier, etc.).Un signe que deux entités sont couplées est que l'une d'entre elles va se briser (ou doit être modifiée) lorsque l'autre est modifiée de quelque manière que ce soit.
L'héritage augmente le couplage, évidemment; changer la classe de base change toutes les sous-classes.
Les interfaces réduisent le couplage: en définissant un contrat clair, basé sur une méthode, vous pouvez modifier librement tout ce qui concerne les deux côtés de l'interface, à condition de ne pas modifier le contrat. (Notez que "interface" est un concept général, le langage Java
interface
classes abstraites ou C ++ ne sont que des détails d'implémentation).Haute cohésion
Cela signifie que chaque classe, objet, fichier, etc. soit concerné ou responsable du moins possible. Autrement dit, évitez les grandes classes qui font beaucoup de choses. Dans votre exemple, si vos armes ont des aspects complètement séparés (munitions, comportement de tir, représentation graphique, représentation d'inventaire, etc.), vous pouvez avoir différentes classes qui représentent exactement l'une de ces choses. La classe d'arme principale se transforme alors en "détenteur" de ces détails; un objet d'arme n'est alors guère plus que quelques indications sur ces détails.
Dans cet exemple, vous vous assurez que votre classe représentant le "comportement de tir" en sait le moins possible sur la classe d'armes principale. De manière optimale, rien du tout. Cela signifierait, par exemple, que vous pourriez donner un "comportement de mise à feu" à n'importe quel objet de votre monde (tourelles, volcans, PNJ, etc.) en un claquement de doigt. Si, à un moment donné, vous souhaitez modifier la manière dont les armes sont représentées dans l'inventaire, vous pouvez simplement le faire - seule votre classe d'inventaire en est consciente.
Un signe qu'une entité n'est pas cohérente est qu'il grandit de plus en plus, se ramifiant dans plusieurs directions en même temps.
L'héritage tel que vous le décrivez diminue la cohésion. Vos classes d'armes sont, en fin de compte, de gros morceaux qui gèrent toutes sortes d'aspects différents et non liés de vos armes.
Les interfaces augmentent indirectement la cohésion en séparant clairement les responsabilités entre les deux côtés de l'interface.
Que faire maintenant
Il n'y a toujours pas de règles strictes, tout cela n'est que des directives. En général, comme l'utilisateur TKK l'a mentionné dans sa réponse, l'héritage est beaucoup enseigné à l'école et dans les livres; ce sont les choses fantaisistes sur la POO. Les interfaces sont probablement plus ennuyeuses à enseigner et aussi (si vous dépassez des exemples triviaux) un peu plus difficiles, ouvrant le champ de l'injection de dépendance, qui n'est pas aussi nette que l'héritage.
En fin de compte, votre système fondé sur l'héritage est toujours préférable à l'absence de conception claire de la POO. Alors n'hésitez pas à vous en tenir à cela. Si vous le souhaitez, vous pouvez réfléchir / google un peu sur le couplage faible, la cohésion élevée et voir si vous souhaitez ajouter ce type de réflexion à votre arsenal. Vous pouvez toujours refacturer pour essayer cela si vous le souhaitez, plus tard; ou essayez des approches basées sur une interface sur votre nouveau module de code plus grand.
la source
L'idée qu'il faut éviter les héritages est tout simplement fausse.
Il existe un principe de codage appelé Composition sur l'héritage . Cela dit que vous pouvez réaliser les mêmes choses avec la composition, et c'est préférable, car vous pouvez réutiliser une partie du code. Voir Pourquoi devrais-je préférer la composition à l'héritage?
Je dois dire que j'aime vos classes d'armes et que cela se ferait de la même manière. Mais je n'ai pas encore créé de jeu ...Comme l'a souligné James Trotter, la composition pourrait présenter certains avantages, notamment en termes de flexibilité lors de l'exécution pour modifier le fonctionnement de l'arme. Ce serait possible avec l'héritage, mais c'est plus difficile.
la source
Le problème est que l'héritage entraîne le couplage - vos objets doivent en savoir plus les uns sur les autres. C'est pourquoi la règle est "toujours privilégier la composition à l'héritage". Cela ne signifie pas que vous n'utiliserez JAMAIS l'héritage, mais que vous l'utiliserez là où c'est tout à fait approprié, mais si jamais vous vous assoyez en pensant: "Je peux le faire dans les deux sens et ils ont tous deux un sens", passez directement à la composition.
L'héritage peut aussi être un peu limitant. Vous avez un "WeaponBase" qui peut être un AssultRifle - génial. Que se passe-t-il lorsque vous avez un fusil à double canon et que vous voulez permettre à ceux-ci de tirer de manière indépendante - un peu plus difficile mais faisable, vous avez juste une classe à un et deux canons, mais vous ne pouvez pas simplement monter 2 canons sur le même pistolet, pouvez-vous? Pourriez-vous en modifier un pour avoir 3 barils ou avez-vous besoin d'une nouvelle classe pour cela?
Qu'en est-il d'un fusil d'assaut avec un GrenadeLauncher dessous - hmm, un peu plus difficile. Pouvez-vous remplacer le GrenadeLauncher par une lampe de poche pour la chasse nocturne?
Enfin, comment permettez-vous à votre utilisateur de créer les armes précédentes en modifiant un fichier de configuration? Cela est difficile car vous avez des relations codées en dur qui pourraient être mieux composées et configurées avec des données.
L'héritage multiple peut résoudre certains de ces exemples triviaux, mais il ajoute son propre ensemble de problèmes.
Avant qu'il y ait des dictons courants tels que "Préfère la composition à l'héritage", j'ai découvert ceci en écrivant un arbre d'héritage trop complexe (qui était génial et qui marchait parfaitement), puis en découvrant qu'il était très difficile de le modifier et de le conserver ultérieurement. Le dicton dit simplement que vous avez un moyen alternatif et compact d’apprendre et de vous souvenir d’un tel concept sans avoir à passer par toute l’expérience - mais si vous souhaitez utiliser l’héritage de manière intensive, je vous recommande de garder l’esprit ouvert et d’évaluer à quel point cela fonctionne. pour vous - rien de terrible ne se produira si vous utilisez fortement l'héritage et ce pourrait être une excellente expérience d'apprentissage au pire (au mieux, cela pourrait fonctionner correctement pour vous)
la source
Monster
une sous-classe deWalkingEntity
. Puis ils ont ajouté des boues. À quel point pensez-vous que cela a fonctionné?Contrairement aux autres réponses, cela n'a rien à voir avec l'héritage ou la composition. L'héritage par rapport à la composition est une décision que vous prenez concernant la manière dont une classe sera mise en œuvre. Les interfaces contre les classes est une décision qui précède.
Les interfaces sont les citoyens de première classe de toutes les langues POO. Les classes sont des détails d'implémentation secondaires. Les enseignants et les livres qui introduisent les classes et l'héritage avant les interfaces déforment gravement l'esprit des nouveaux programmeurs.
Le principe clé est que, chaque fois que cela est possible, les types de tous les paramètres de méthode et les valeurs de retour doivent être des interfaces et non des classes. Cela rend vos API beaucoup plus flexibles et puissants. La plupart du temps, vos méthodes et leurs appelants ne doivent avoir aucune raison de connaître ou de ne pas se soucier des classes réelles des objets avec lesquels ils traitent. Si votre API est agnostique à propos des détails d'implémentation, vous pouvez librement basculer entre composition et héritage sans rien casser.
la source
interface
(le mot-clé du langage).Chaque fois que quelqu'un vous dit qu'une approche spécifique est la meilleure pour tous les cas, c'est la même chose que de vous dire qu'un seul et même médicament guérit toutes les maladies.
Héritage vs composition est une question est-a vs has-a. Les interfaces sont un autre moyen (3ème) approprié à certaines situations.
L'héritage, ou la logique is-a: vous l'utilisez quand la nouvelle classe va se comporter et être utilisé complètement comme (extérieurement) l'ancienne classe dont vous dérivez, si la nouvelle classe va exposer au public toutes les fonctionnalités que l'ancienne classe avait ... alors vous héritez.
Composition ou logique du has-a: si la nouvelle classe a juste besoin d'utiliser en interne l'ancienne classe sans exposer les fonctionnalités de l'ancienne classe au public, vous utilisez la composition, c'est-à-dire que vous avez une instance de l'ancienne classe propriété membre ou une variable de la nouvelle. (Cela peut être une propriété privée, ou protégée, ou autre, selon le cas d'utilisation). Le point clé ici est qu'il s'agit du cas d'utilisation dans lequel vous ne souhaitez pas exposer les fonctionnalités de classe d'origine et les utiliser pour le public, mais les utiliser en interne, alors que dans le cas d'héritage, vous les exposez au public, via le nouvelle classe. Parfois, vous avez besoin d'une approche et parfois de l'autre.
Interfaces: les interfaces constituent un autre cas d'utilisation - lorsque vous souhaitez que la nouvelle classe implémente partiellement et non complètement et n'expose pas au public les fonctionnalités de l'ancienne. Cela vous permet d’avoir une nouvelle classe, une classe d’une hiérarchie totalement différente de l’ancienne, se comporte comme l’ancienne sous certains aspects seulement.
Supposons que vous avez un assortiment de créatures représentées par des classes et que leurs fonctionnalités sont représentées par des fonctions. Par exemple, un oiseau aurait Talk (), Fly () et Merde (). La classe Canard hériterait de la classe Bird, mettant en œuvre toutes les fonctionnalités.
La classe Fox, évidemment, ne peut pas voler. Donc, si vous définissez l'ancêtre comme ayant toutes les fonctionnalités, vous ne pouvez pas dériver le descendant correctement.
Si, toutefois, vous divisez les entités en groupes, représentant chaque groupe d'appels avec une interface, par exemple, IFly, contenant Takeoff (), FlapWings () et Land (), vous pouvez pour la classe Fox implémenter des fonctions à partir de ITalk et IPoop. mais pas IFly. Vous définissez ensuite les variables et les paramètres pour accepter les objets qui implémentent une interface spécifique, puis le code qui les utilise sait ce qu’il peut appeler ... et peut toujours rechercher d’autres interfaces, s’il doit voir si d’autres fonctionnalités le sont également. mis en œuvre pour l'objet actuel.
Chacune de ces approches a des cas d'utilisation quand c'est la meilleure, aucune approche n'est la meilleure solution absolue pour tous les cas.
la source
Pour un jeu, en particulier avec Unity, qui fonctionne avec une architecture à composants d'entités, vous devez privilégier la composition plutôt que l'héritage pour vos composants de jeu. Il est beaucoup plus flexible et évite de tomber dans une situation où vous souhaitez qu'une entité de jeu «soit» deux éléments situés sur des feuilles différentes d'un arbre d'héritage.
Supposons, par exemple, que vous avez une TalkingAI sur une branche d’un arbre d’héritage, et que VolumetricCloud se trouve sur une autre branche distincte et que vous souhaitez un nuage parlant. C'est difficile avec un arbre d'héritage profond. Avec entité-composant, vous créez simplement une entité qui comporte des composants TalkingAI et Cloud, et vous êtes prêt à partir.
Mais cela ne signifie pas que, par exemple, dans votre implémentation de nuage volumétrique, vous ne devriez pas utiliser l'héritage. Le code correspondant peut être substantiel et se composer de plusieurs classes. Vous pouvez utiliser OOP selon vos besoins. Mais tout cela équivaudra à une seule composante du jeu.
En note de bas de page, je ne suis pas d'accord avec ceci:
L'héritage n'est pas destiné à la réutilisation de code. C'est pour établir une relation is-a . Souvent, cela va de pair, mais il faut faire attention. Le fait que deux modules aient besoin d'utiliser le même code ne signifie pas que l'un est du même type que l'autre.
Vous pouvez utiliser des interfaces si vous voulez avoir, par exemple,
List<IWeapon>
différents types d’armes. De cette manière, vous pouvez hériter de l'interface IWeapon et de la sous-classe MonoBehaviour pour toutes les armes et éviter tout problème lié à l'absence d'héritage multiple.la source
Vous semblez ne pas comprendre ce qu'une interface est ou fait. Il m'a fallu environ 10 ans de réflexion sur OO et poser des questions à des personnes très intelligentes pour savoir si je pouvais avoir un moment ah-ha avec des interfaces. J'espère que cela t'aides.
Le mieux est d’en utiliser une combinaison. Dans mon esprit, modéliser le monde et les gens, disons un exemple profond. L'âme est interfacée avec le corps à travers l'esprit. Le mental EST l'interface entre l'âme (certains considèrent l'intellect) et les ordres donnés au corps. L'interface des esprits avec le corps est la même pour toutes les personnes, fonctionnellement parlant.
La même analogie peut être utilisée entre la personne (corps et âme) en interface avec le monde. Le corps est l'interface entre l'esprit et le monde. Tout le monde s'interface de la même manière avec le monde. La seule particularité est la façon dont nous interagissons avec le monde. C'est ainsi que nous utilisons notre esprit pour décider comment nous interagissons / interagissons dans le monde.
Une interface est alors simplement un mécanisme permettant de relier votre structure OO à une autre structure OO différente, chaque côté ayant des attributs différents qui doivent être négociés / mappés les uns aux autres pour produire une sortie fonctionnelle dans un milieu / environnement différent.
Donc, pour que l'esprit appelle la fonction open-hand, il doit implémenter l'interface nerveuse_command_system avec QUE, ayant sa propre API avec des instructions sur la manière de mettre en œuvre toutes les exigences nécessaires avant d'appeler open-hand.
la source