Est-il acceptable d'avoir des odeurs de code s'il admet une solution plus facile à un autre problème? [fermé]


31

Un groupe d'amis et moi travaillons depuis peu sur un projet, et nous voulions inventer une belle façon OOP de représenter un scénario spécifique à notre produit. Fondamentalement, nous travaillons sur un jeu d'enfer de balle de style Touhou , et nous voulions créer un système où nous pourrions facilement représenter tout comportement possible de balle que nous pourrions imaginer.

C'est exactement ce que nous avons fait; nous avons fait une architecture vraiment élégante qui nous a permis de séparer le comportement d'une balle en différents composants qui pourraient être attachés aux instances de balle à volonté, un peu comme le système de composants d'Unity . Il fonctionnait bien, il était facilement extensible, il était flexible et couvrait toutes nos bases, mais il y avait un léger problème.

Notre application implique également une grande quantité de génération de procédures, à savoir que nous générons de manière procédurale les comportements des balles. Pourquoi c'est un problème? Eh bien, notre solution de POO pour représenter le comportement des balles, tout en étant élégante, est un peu compliquée à travailler sans être humain. Les humains sont suffisamment intelligents pour penser à des solutions à des problèmes à la fois logiques et intelligents. Les algorithmes de génération procédurale ne sont pas encore aussi intelligents, et nous avons eu du mal à implémenter une IA qui utilise notre architecture OOP à son plein potentiel. Certes, c'est une faille de l'architecture, c'est qu'elle n'est pas intuitive dans toutes les situations.

Donc, pour remédier à ce problème, nous avons essentiellement poussé tous les comportements offerts par différents composants dans la classe de puce, de sorte que tout ce que nous pouvions imaginer soit proposé directement dans chaque instance de puce, par opposition à dans d'autres instances de composants associées. Cela rend nos algorithmes de génération procédurale un peu plus faciles à utiliser, mais maintenant notre classe bullet est un énorme objet divin . C'est de loin la classe la plus importante du programme avec plus de cinq fois plus de code qu'autre chose. C'est aussi un peu pénible à maintenir.

Est-il normal qu'une de nos classes se transforme en objet divin, juste pour faciliter le travail avec un autre problème? En général, est-il acceptable d'avoir des odeurs de code dans votre code s'il admet une solution plus facile à un problème différent?


1
Eh bien ... c'est un peu difficile à répondre. À mon avis, NON. Surtout si vous travaillez avec d'autres personnes. Si vous travaillez seul, vous pouvez faire ce que vous voulez. Mais en général, cela dépend de qui vous travaillez.
Sarcastic Potato

13
"Si c'est stupide et que ça marche, ce n'est pas stupide." Cela étant dit, vous devez prendre en considération que même si cela fonctionne exactement comme vous le souhaitez, que vous ne rendiez pas le codage futur sur cette classe divine presque impossible en raison de la façon dont vous avez décidé de le réparer.
Flater

8
Il vaut mieux sentir et savoir que tu pues, puis ne sentir que les autres. :)
Reactgular

1
On dirait que vous avez besoin d'une classe d'adaptateur qui fournit une interface non-OO à votre code procédural, afin que vous puissiez garder le reste de votre code propre (euh).
nikie

1
Il semble que vous ayez construit un système merveilleux et décidé de tout faire exploser maintenant, il ne fonctionne plus comme prévu sur une fonctionnalité. C'est vraiment ce que tu veux? Soyez fier de votre travail!
mât

Réponses:


74

Lors de la création de programmes du monde réel, il y a souvent un compromis entre rester pragmatique d'une part et rester 100% propre de l'autre. Si rester propre vous interdit d'expédier votre produit à temps, il vaut mieux utiliser un peu de ruban adhésif pour sortir le truc du d *** d de la porte.

Cela dit, votre description semble différente - il semble que vous n'allez pas ajouter un peu de ruban adhésif, il semble que vous allez ruiner toute votre architecture parce que vous n'avez pas l'air assez long et dur pour une meilleure solution. Donc, au lieu de chercher quelqu'un ici sur PSE qui vous donne une bénédiction à ce sujet, vous feriez mieux de poser une question différente où vous décrivez en détail certains des problèmes que vous avez en profondeur, et regardez si quelqu'un vous propose une idée qui évite le dieu approche par classe.

Peut-être que la classe de balle peut être conçue pour être la façade d'un tas d'autres classes, de sorte que la classe de balle devient plus petite. Peut-être que le modèle de stratégie peut aider afin que la puce puisse déléguer différents comportements à différents objets de stratégie. Peut-être avez-vous juste besoin d'un adaptateur entre votre composant bullet et votre générateur de procédures. Mais honnêtement, sans connaître plus de détails sur votre système, on ne peut que deviner.


6
Juste pour réaffirmer cette réponse, j'allais écrire quelque chose avec les deux mêmes recommandations. Concernant le dernier paragraphe, ce que OP décrit semble être une odeur de code en soi indiquant qu'un autre niveau d'indirection est nécessaire. Que ce soit une meilleure classe d'usine, des façades ou même un DSL complet que l'IA peut générer est laissé à l'OP.
Daniel B

2
Bonnes suggestions sur la façade / stratégie pour diviser le code en petits morceaux. Mais sans en savoir plus, il est difficile de savoir quoi suggérer.
user949300

25

Question interessante. Je suis un peu biaisé cependant à cause de mes expériences précédentes, ce qui m'incite à répondre par non.

Réponse courte: Nous n'arrêtons jamais d'apprendre. Lorsque vous frappez un mur comme ça, c'est une chance d'améliorer vos compétences en architecture / conception, pas une excuse pour ajouter des odeurs de code.

La version plus longue est que l'on m'a souvent posé des questions similaires dans mes projets au travail, mais inévitablement, nous avons fini par discuter de la question de manière beaucoup plus approfondie, puis le questionneur d'origine l'a reconnu. Vous voulez inclure des mentalités comme l'analyse des causes profondes avec cela.

J'ai trouvé les 5 pourquoi particulièrement utiles. Votre explication ici se lit comme le premier pourquoi:

  • "Nous avons cette classe de dieu maintenant" Pourquoi?
  • "Parce que cela simplifie l'algorithme de génération procédurale"

Qu'est-ce qui est exactement simplifié? Et pourquoi en est-il ainsi? Continuez dans cette voie et ce qui se produit généralement, c'est que vous commencez à reconnaître des problèmes beaucoup plus fondamentaux, mais en même temps, ils vous permettent également des solutions plus fondamentales.

Pour faire court, après avoir réfléchi plus profondément au problème en question et en particulier à ses causes, d'après mon expérience, la question initiale a fini par être nulle et totalement inintéressante pour tous les participants. Si vous avez des doutes, défiez-les et soit ils disparaîtront, soit vous saurez ce que vous devez faire. Attention, c'est un bon signe à mon avis d'avoir ces doutes sur le code / design. D'autres appellent cela un sentiment d'intestin, mais pour contester ces problèmes, vous devez d'abord les reconnaître. Depuis que vous avez fait cette première étape, félicitations! Maintenant, descendez le terrier du lapin ...


9

Avoir une classe divine comme celle-ci n'est jamais souhaitable, car cela ne signifie pas seulement que vos balles sont maintenant des objets monolithiques, mais il en va de même pour votre algorithme de génération procédurale.

La première étape aurait été d'analyser, pourquoi exactement votre IA a-t-elle eu tant de mal à gérer la complexité de votre modèle?

Avez-vous, par hasard, essayé de transformer votre IA en un objet de classe divine, pleinement conscient de la sémantique de chaque propriété possible? Si c'est le cas, c'est à l'origine du problème.

La solution n'aurait alors pas été d'intégrer toutes les stratégies dans la classe à puces elle-même, mais de décharger plutôt l'indication pour l'IA du cœur de l'IA aux implémentations de stratégie elles-mêmes.

Cela vous aurait donné la flexibilité que vous vouliez à l'origine, y compris la possibilité d'étendre le système par de nouvelles stratégies comme vous le souhaitez sans craindre de rencontrer des effets secondaires avec des comportements hérités.

Au lieu de cela, vous avez maintenant tous les problèmes associés aux objets divins: Non seulement votre classe divine elle-même est un monolithe difficile à comprendre, difficile à déboguer, mais il en va de même pour tous les autres composants accédant à une telle classe divine. En raison du manque d'abstraction, votre IA est maintenant appelée à se transformer en une abomination de complexité similaire car elle doit être consciente de toutes les propriétés individuelles redondantes maintenant.

Même en ce moment, vous rencontrez déjà des problèmes de maintenance. Ces problèmes s'aggravent, surtout dès que vous perdez les membres de l'équipe qui ont encore un modèle cognitif de la façon dont cette classe est censée fonctionner.

Jusqu'à présent, tous les projets que j'ai rencontrés qui utilisaient de telles classes ou fonctions divines ont été complètement réécrits à partir de zéro ou ont cessé, sans exception.


Une chose qui manque souvent dans les discussions sur la taille ou la complexité des classes devrait être une distinction entre "combien de code devrait contenir une référence de type X" et "beaucoup de logique devrait être dans le fichier de classe pour X". Si un client aurait probablement des collections d'objets qui prennent en charge une variété de capacités (par exemple "fnorble"), et se retrouve souvent à vouloir demander à tous les membres d'une collection de par exemple "fnorble si possible, sinon ne rien faire", puis avoir une interface qui combine plusieurs de ces méthodes peut être beaucoup plus utile que d'essayer de diviser l'interface.
supercat

Avec une telle interface, il est possible que le code encapsule des objets avec n'importe quelle combinaison de capacités, sans avoir à gérer séparément différentes combinaisons. La séparation des interfaces rendrait nécessaire l'écriture de nombreuses classes de proxy différentes, car les proxy pour un type qui prend en charge la méthode X ne peuvent pas être utilisés pour encapsuler des objets qui ne peuvent pas X, et les proxy qui peuvent encapsuler des objets qui ne peuvent pas X ne le feraient pas. t être en mesure d'exposer la méthode X même si elle encapsulait un objet pouvant la prendre en charge.
supercat

8

Tout mauvais code depuis la nuit des temps a une histoire derrière son évolution qui lui donne un aspect raisonnable étape par étape. Le vôtre ne fait pas exception. Les codeurs apprennent en codant. Il y a des aspects de votre problème que vous n'auriez pas pu prévoir qui semblent évidents maintenant. Il y a des décisions que vous avez prises qui étaient tout à fait raisonnables progressivement, mais qui ont conduit votre architecture dans la mauvaise direction dans l'ensemble. Vous devez identifier et réexaminer ces décisions.

Demandez à votre bureau si les gens ont des idées pour le réparer. Dans la plupart des endroits où j'ai travaillé, environ 10 à 20% des programmeurs ont un vrai talent pour ce genre de choses, et n'attendent que l'occasion. Découvrez qui sont ces personnes. Souvent, ce sont vos nouvelles recrues qui ne sont pas historiquement investies dans l'architecture actuelle qui peuvent le plus facilement voir des alternatives. Associez-les à l'un de vos anciens combattants et vous pourriez être étonné de ce qu'ils proposent ensemble.


2

Dans certains cas, cela est tout à fait acceptable. Cependant, j'ai du mal à croire qu'il n'y a pas de bonne solution en utilisant à la fois la génération procédurale et votre belle architecture de comportement basée sur les composants / attachés. Si tous les comportements venaient d'être introduits dans la classe de balle, il n'y a pas de différence fonctionnelle entre l'objet divin et la version architecturée soignée. Qu'est-ce qui a rendu l'utilisation de vos algorithmes de génération de procédures si difficile?

Je pense à une nouvelle question (peut-être ici, ou sur gamedev.stackexchange.com?), Où vous décrivez les problèmes que vous avez rencontrés avec votre architecture en combinaison avec proc. gen., serait vraiment intéressant. Faites-nous savoir ici si vous posez une nouvelle question!


3
Pouvez-vous donner un exemple d'une situation où avoir une classe divine serait à la fois acceptable et souhaitable alors que cette classe n'est pas seulement une compilation automatisée de sources individuelles?
Ext3h

Avec acceptable, je faisais référence à "Est-il acceptable d'avoir des odeurs de code s'il admet une solution plus facile à un autre problème?" Les classes divines sont généralement facilement résolubles en utilisant la composition. Mais oui, il y a certaines odeurs de code qui ne valent vraiment pas la peine de se débarrasser à 100%. En fin de compte, un peu de pragmatisme est nécessaire pour faire le travail :). (Cela ne signifie pas que je pense que vous devriez rejeter les meilleures pratiques sur un coup de tête. Je pense que la plupart des éditeurs de logiciels feraient mieux de suivre les meilleures pratiques plus strictement).
Roy T.

0

Pour vous inspirer, vous voudrez peut-être jeter un œil à la programmation fonctionnelle ou, plus précisément, aux types sur mesure. L'idée étant que les choses ne fonctionnent pas est impossible à représenter dans le système de type dont vous disposez. Par exemple, disons que vous avez un pistolet qui a les composants "tirer des balles" et "tenir des balles". S'il s'agit de deux composants distincts, il y a des configurations qui n'ont aucun sens - vous pouvez avoir un pistolet qui tire des balles mais n'en a pas dans son stockage, ou un pistolet qui stocke des balles mais ne les tire pas.

À ce niveau de complexité, vous pensez "à droite, un humain comprendra que cela est inutile et évitera les combinaisons impossibles". Une meilleure façon pourrait être de rendre cela impossible. Par exemple, le composant pour "tirer des balles" peut nécessiter une référence au composant "tenir des balles" (ou simplement le maintenir comme une partie de lui-même, bien qu'il ait ses propres problèmes).

Bien que vos exemples puissent être beaucoup plus compliqués, la clé limite toujours les combinaisons possibles, en ajoutant des contraintes. D'une certaine manière, si c'est trop compliqué pour que la génération procédurale se passe bien, c'est probablement trop compliqué de toute façon. Pensez à toutes les façons dont vous vous contraignez lors de la conception des armes - vous dites-vous "à droite, ce pistolet a déjà un lance-flammes, il est inutile d'ajouter un lance-grenades"? C'est quelque chose que vous pouvez intégrer à votre heuristique. Vous voudrez peut-être utiliser un mécanisme de probabilité un peu plus compliqué que de simplement donner une chance fixe d'avoir chaque composant - vous pourriez avoir des composants qui ont tendance à s'exclure les uns des autres, ou des composants qui ont tendance à bien fonctionner ensemble.

Et enfin, demandez-vous s'il s'agit en fait d'un problème suffisamment important. Est-ce que cela gâche le plaisir du jeu? Les pistolets résultants sont-ils inutiles ou trop puissants? Combien? Un sur 10 est-il insensé? Des jeux comme Borderlands (avec des effets d'armes générés de manière procédurale) ignorent souvent les combinaisons d'effets absurdes - quel est l'intérêt d'avoir un fusil de chasse avec une portée 16x? Et pourtant, cela arrive très souvent à Borderlands. C'est juste joué pour rire, plutôt que d'être considéré comme un échec des mécanismes de génération :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.