J'ai récemment eu une discussion avec un de mes amis sur la POO dans le développement de jeux vidéo.
J'expliquais l'architecture d'un de mes jeux qui, à la surprise de mon ami, contenait de nombreuses petites classes et plusieurs couches d'abstraction. J'ai fait valoir que c'était le résultat de ma concentration sur tout donner une responsabilité unique et aussi pour desserrer le couplage entre les composants.
Il craignait que le grand nombre de cours ne se traduise par un cauchemar de maintenance. À mon avis, cela aurait exactement l'effet inverse. Nous avons procédé à la discussion de ce qui semblait être des siècles, finissant par accepter d'être en désaccord, en disant qu'il y avait peut-être des cas où les principes SOLIDES et la POO appropriée ne se mélangeaient pas vraiment bien.
Même l'entrée de Wikipédia sur les principes SOLID déclare qu'il s'agit de lignes directrices qui aident à écrire du code maintenable et qu'elles font partie d'une stratégie globale de programmation agile et adaptative.
Donc, ma question est:
Y a-t-il des cas dans la POO où certains ou tous les principes SOLID ne se prêtent pas au nettoyage du code?
Je peux imaginer tout de suite que le principe de substitution de Liskov pourrait éventuellement entrer en conflit avec une autre saveur de l'héritage sûr. C'est-à-dire que si quelqu'un a conçu un autre modèle utile implémenté par héritage, il est fort possible que le LSP soit en conflit direct avec lui.
Y en a-t-il d'autres? Peut-être que certains types de projets ou certaines plateformes cibles fonctionnent mieux avec une approche moins SOLIDE?
Modifier:
Je voudrais juste préciser que je ne demande pas comment améliorer mon code;) La seule raison pour laquelle j'ai mentionné un projet dans cette question était de donner un peu de contexte. Ma question concerne la POO et les principes de conception en général .
Si vous êtes curieux de mon projet, voyez ceci .
Modifier 2:
J'ai imaginé que cette question recevrait une réponse de 3 manières:
- Oui, il existe des principes de conception OOP qui entrent partiellement en conflit avec SOLID
- Oui, il existe des principes de conception OOP qui entrent complètement en conflit avec SOLID
- Non, SOLID est le genou de l'abeille et OOP s'en sortira toujours mieux. Mais, comme pour tout, ce n'est pas une panacée. Boire de façon responsable.
Les options 1 et 2 auraient probablement généré des réponses longues et intéressantes. L'option 3, en revanche, serait une réponse courte, sans intérêt mais globalement rassurante.
Nous semblons converger vers l'option 3.
la source
Réponses:
En général, non. L'histoire a montré que les principes SOLID contribuent tous en grande partie à un découplage accru, qui à son tour a montré qu'il augmentait la flexibilité du code et donc votre capacité à s'adapter au changement et à rendre le code plus facile à raisonner, à tester, à réutiliser. En bref, nettoyez votre code.
Maintenant, il peut y avoir des cas où les principes SOLIDES entrent en collision avec DRY (ne vous répétez pas), KISS (gardez les choses stupides) ou d'autres principes de bonne conception OO. Et bien sûr, ils peuvent entrer en collision avec la réalité des exigences, les limites des humains, les limites de nos langages de programmation, d'autres obstacles.
En bref, les principes SOLID se prêteront toujours au code propre, mais dans certains scénarios, ils se prêteront moins que des alternatives conflictuelles. Ils sont toujours bons, mais parfois d'autres choses sont plus bonnes.
la source
Count
etasImmutable
, et des propriétés commegetAbilities
[dont le retour indiquerait si des choses commeCount
seront "efficaces"], alors on pourrait avoir une méthode statique qui prend plusieurs séquences et les agrège pour qu'elles se comportera comme une seule séquence plus longue. Si de telles capacités sont présentes dans le type de séquence de base, même si elles ne seJe pense que j'ai une perspective inhabituelle en ce sens que j'ai travaillé à la fois dans la finance et les jeux.
Beaucoup de programmeurs que j'ai rencontrés dans les jeux étaient terribles en génie logiciel - mais ils n'avaient pas besoin de pratiques comme SOLID. Traditionnellement, ils mettent leur jeu dans une boîte - puis ils ont terminé.
En finance, j'ai trouvé que les développeurs peuvent être très bâclés et indisciplinés car ils n'ont pas besoin des performances que vous faites dans les jeux.
Les deux déclarations ci-dessus sont bien sûr des généralisations excessives, mais en fait, dans les deux cas, un code propre est essentiel. Pour une optimisation, un code clair et facile à comprendre est essentiel. Par souci de maintenabilité, vous voulez la même chose.
Cela ne veut pas dire que SOLID n'est pas sans critique. On les appelle des principes et pourtant ils sont censés être suivis comme des lignes directrices? Alors, quand dois-je les suivre exactement? Quand dois-je enfreindre les règles?
Vous pourriez probablement regarder deux morceaux de code et dire lequel est le mieux compatible avec SOLID. Mais isolément, il n'y a aucun moyen objectif de transformer le code en SOLIDE. C'est définitivement à l'interprétation du développeur.
Il n'est pas logique de dire que "un grand nombre de classes peut conduire à un cauchemar de maintenance". Cependant, si SRP n'est pas correctement interprété, vous pourriez facilement vous retrouver avec ce problème.
J'ai vu du code avec de nombreuses couches à peu près sans responsabilité. Les classes qui ne font rien à part passer l'état d'une classe à l'autre. Le problème classique de trop de couches d'indirection.
SRP en cas d'abus peut se traduire par un manque de cohésion dans votre code. Vous pouvez le savoir lorsque vous essayez d'ajouter une fonctionnalité. Si vous devez toujours changer plusieurs endroits en même temps, votre code manque de cohésion.
Open-Closed n'est pas sans critiques. Par exemple, voir la revue de Jon Skeet du principe Open Closed . Je ne reproduirai pas ses arguments ici.
Votre argument sur LSP semble plutôt hypothétique et je ne comprends pas vraiment ce que vous entendez par une autre forme d'héritage sûr?
ISP semble reproduire quelque peu SRP. Ils doivent certainement être interprétés de la même manière, car vous ne pouvez jamais prévoir ce que feront tous les clients de votre interface. L'interface cohérente d'aujourd'hui est le violeur de FAI de demain. La seule conclusion illogique est de ne pas avoir plus d'un membre par interface.
DIP peut conduire à un code inutilisable. J'ai vu des classes avec un grand nombre de paramètres au nom de DIP. Ces classes auraient normalement "renouvelé" leurs parties composites, mais pour le nom de la capacité de test, nous ne devons plus jamais utiliser le nouveau mot-clé.
SOLID ne suffit pas à lui seul pour écrire du code propre. J'ai vu le livre de Robert C. Martin, « Clean Code: A Handbook of Agile Software Craftsmanship ». Le plus gros problème est qu'il est facile de lire ce livre et de l'interpréter comme des règles. Si vous faites cela, vous manquez le point. Il contient une grande liste d'odeurs, d'heuristiques et de principes - bien plus que les cinq de SOLID. Tous ces «principes» ne sont pas vraiment des principes mais des lignes directrices. L'oncle Bob les décrit lui-même comme des "trous cubiques" pour les concepts. C'est un jeu d'équilibre; suivre aveuglément toute directive entraînera des problèmes.
la source
Voici mon avis:
Bien que les principes SOLID visent une base de code non redondante et flexible, cela pourrait être un compromis en termes de lisibilité et de maintenance s'il y a trop de classes et de couches.
Il s'agit surtout du nombre d' abstractions . Un grand nombre de classes pourraient être correctes si elles sont basées sur quelques abstractions. Comme nous ne connaissons pas la taille et les spécificités de votre projet, c'est difficile à dire, mais il est un peu inquiétant que vous mentionniez plusieurs couches en plus d'un grand nombre de classes.
Je suppose que les principes SOLIDES ne sont pas en désaccord avec la POO, mais il est toujours possible d'écrire du code non propre en y adhérant.
la source
Dans mes premiers jours de développement, j'ai commencé à écrire un Roguelike en C ++. Comme je voulais appliquer la bonne méthodologie orientée objet que j'ai apprise de mon éducation, j'ai immédiatement vu les éléments du jeu comme des objets C ++.
Potion
,Swords
,Food
,Axe
,Javelins
Etc. tous dérivés d'un noyau de base duItem
code, le nom de manipulation, l' icône, le poids, ce genre de choses.Ensuite, j'ai codé la logique du sac et j'ai rencontré un problème. Je peux mettre
Items
dans le sac, mais ensuite, si j'en choisis un, comment savoir si c'est unPotion
ou unSword
? J'ai cherché sur Internet comment abattre des éléments. J'ai piraté les options du compilateur pour activer les informations d'exécution afin que je sache quels sont mesItem
vrais types abstraits , et j'ai commencé à basculer la logique sur les types pour savoir quelles opérations je permettrais (par exemple, éviter de boireSwords
et de mettre lesPotions
pieds)Il a essentiellement transformé le code en une grosse boule de boue à moitié copypastée. Au fur et à mesure que le projet progressait, j'ai commencé à comprendre quel était l'échec de la conception. Ce qui s'est passé, c'est que j'ai essayé d'utiliser des classes pour représenter des valeurs. Ma hiérarchie de classe n'était pas une abstraction significative. Ce que je aurais dû faire fait de
Item
mettre en œuvre un ensemble de fonctions, telles queEquip ()
,Apply ()
etThrow ()
, et font de chaque se comportent en fonction des valeurs. Utiliser des énumérations pour représenter les emplacements d'équipement. Utiliser des énumérations pour représenter différents types d'armes. Utiliser plus de valeurs et moins de classement, car ma sous-classification n'avait d'autre but que de remplir ces valeurs finales.Étant donné que vous êtes confronté à la multiplication d'objets sous une couche d'abstraction, je pense que mes informations peuvent être précieuses ici. Si vous semblez avoir trop de types d'objets, il se peut que vous confondiez ce qui devrait être des types avec ce qui devrait être des valeurs.
la source
Je suis absolument du côté de votre ami, mais cela pourrait être une question de nos domaines et des types de problèmes et de conceptions que nous abordons et en particulier des types de choses susceptibles de nécessiter des changements à l'avenir. Différents problèmes, différentes solutions. Je ne crois pas au bien ou au mal, juste aux programmeurs qui essaient de trouver la meilleure façon de résoudre au mieux leurs problèmes de conception particuliers. Je travaille dans VFX qui n'est pas trop différent des moteurs de jeux.
Mais le problème avec lequel je me suis débattu dans ce qui pourrait au moins être appelé un peu plus une architecture conforme à SOLID (elle était basée sur COM), pourrait se résumer grossièrement à "trop de classes" ou "trop de fonctions" comme votre ami pourrait décrire. Je dirais spécifiquement: "trop d'interactions, trop d'endroits qui pourraient éventuellement mal se comporter, trop d'endroits qui pourraient éventuellement provoquer des effets secondaires, trop d'endroits qui pourraient devoir changer, et trop d'endroits qui pourraient ne pas faire ce que nous pensons qu'ils font . "
Nous avions une poignée d'interfaces abstraites (et pures) implémentées par une cargaison de sous-types, comme ça (faites ce diagramme dans le contexte de parler des avantages ECS, ne tenez pas compte du commentaire en bas à gauche):
Où une interface de mouvement ou une interface de nœud de scène peut être implémentée par des centaines de sous-types: lumières, caméras, maillages, solveurs physiques, shaders, textures, os, formes primitives, courbes, etc. etc. (et il y avait souvent plusieurs types de chacun ). Et le problème ultime était vraiment que ces conceptions n'étaient pas si stables. Nous avions des exigences changeantes et parfois les interfaces elles-mêmes devaient changer, et quand vous voulez changer une interface abstraite implémentée par 200 sous-types, c'est un changement extrêmement coûteux. Nous avons commencé à atténuer cela en utilisant des classes de base abstraites entre les deux, ce qui a réduit les coûts de ces modifications de conception, mais elles étaient toujours coûteuses.
J'ai donc commencé à explorer l'architecture de système à composants d'entité utilisée assez couramment dans l'industrie du jeu. Cela a tout changé pour ressembler à ceci:
Et wow! C'était une telle différence en termes de maintenabilité. Les dépendances ne coulaient plus vers les abstractions , mais vers les données (composants). Et dans mon cas au moins, les données étaient beaucoup plus stables et plus faciles à obtenir en termes de conception dès le départ malgré les exigences changeantes (bien que ce que nous pouvons faire avec les mêmes données change constamment avec l'évolution des exigences).
De plus, étant donné que les entités d'un ECS utilisent la composition au lieu de l'héritage, elles n'ont en fait pas besoin de contenir de fonctionnalité. Ils ne sont que le "conteneur de composants" analogique. Cela a fait en sorte que les 200 sous-types analogiques qui ont implémenté une interface de mouvement se transforment en 200 instances d' entité (pas des types séparés avec un code séparé) qui stockent simplement un composant de mouvement (qui n'est rien d'autre que des données associées au mouvement). A
PointLight
n'est plus une classe / sous-type distinct. Ce n'est pas du tout une classe. C'est une instance d'une entité qui combine simplement certains composants (données) liés à sa position dans l'espace (mouvement) et aux propriétés spécifiques des lumières ponctuelles. La seule fonctionnalité qui leur est associée se trouve à l'intérieur des systèmes, comme leRenderSystem
, qui recherche des composants lumineux dans la scène pour déterminer comment rendre la scène.Avec l'évolution des exigences dans le cadre de l'approche ECS, il n'était souvent nécessaire de changer qu'un ou deux systèmes fonctionnant sur ces données ou simplement d'introduire un nouveau système sur le côté, ou d'introduire un nouveau composant si de nouvelles données étaient nécessaires.
Donc pour mon domaine au moins, et je suis presque certain que ce n'est pas pour tout le monde, cela a rendu les choses tellement plus faciles parce que les dépendances coulaient vers la stabilité (des choses qui n'avaient pas besoin de changer souvent du tout). Ce n'était pas le cas dans l'architecture COM lorsque les dépendances circulaient uniformément vers les abstractions. Dans mon cas, il est beaucoup plus facile de déterminer quelles données sont nécessaires pour le mouvement initial plutôt que toutes les choses possibles que vous pourriez en faire, ce qui change souvent un peu au fil des mois ou des années à mesure que de nouvelles exigences arrivent.
Eh bien, un code propre, je ne peux pas le dire, car certaines personnes assimilent le code propre à SOLID, mais il y a certainement des cas où la séparation des données des fonctionnalités comme le fait ECS et la redirection des dépendances des abstractions vers les données peuvent certainement rendre les choses beaucoup plus faciles à changer, pour des raisons évidentes de couplage, si les données vont être beaucoup plus stables que les abstractions. Bien sûr, les dépendances aux données peuvent rendre la maintenance des invariants difficile, mais ECS a tendance à atténuer cela au minimum avec l'organisation du système qui minimise le nombre de systèmes qui accèdent à n'importe quel type de composant.
Ce n'est pas nécessairement que les dépendances devraient se diriger vers les abstractions comme le suggère DIP; les dépendances devraient se diriger vers des choses qui ne nécessiteront probablement pas de changements futurs. Cela peut ou non être des abstractions dans tous les cas (ce n'était certainement pas dans le mien).
Je ne sais pas si ECS est vraiment une saveur de POO. Certaines personnes le définissent de cette façon, mais je le vois comme très différent par nature avec les caractéristiques de couplage et la séparation des données (composants) de la fonctionnalité (systèmes) et le manque d'encapsulation des données. Si elle doit être considérée comme une forme de POO, je pense qu'elle est très en conflit avec SOLID (au moins les idées les plus strictes de SRP, ouvert / fermé, substitution de liskov et DIP). Mais j'espère que c'est un exemple raisonnable d'un cas et d'un domaine où les aspects les plus fondamentaux de SOLID, au moins tels que les gens les interpréteraient généralement dans un contexte de POO plus reconnaissable, pourraient ne pas être aussi applicables.
Teeny Classes
L'ECS a beaucoup défié et changé mes vues. Comme vous, je pensais que l'idée même de la maintenabilité est d'avoir l'implémentation la plus simple possible des choses, ce qui implique beaucoup de choses, et en plus, beaucoup de choses interdépendantes (même si les interdépendances sont entre les abstractions). Cela a plus de sens si vous zoomez sur une seule classe ou fonction pour vouloir voir l'implémentation la plus simple et la plus simple, et si nous n'en voyons pas, refactorisez-la et peut-être même décomposez-la davantage. Mais il peut être facile de passer à côté de ce qui se passe avec le monde extérieur, car chaque fois que vous divisez quelque chose de relativement complexe en 2 choses ou plus, ces 2 choses ou plus doivent inévitablement interagir * (voir ci-dessous) les unes avec les autres dans certains façon, ou quelque chose à l'extérieur doit interagir avec chacun d'eux.
Ces jours-ci, je trouve qu'il y a un équilibre entre la simplicité de quelque chose et combien de choses il y a et combien d'interaction est nécessaire. Les systèmes dans un ECS ont tendance à être assez lourds avec des implémentations non triviales pour fonctionner sur les données, comme
PhysicsSystem
ouRenderSystem
ouGuiLayoutSystem
. Cependant, le fait qu'un produit complexe en ait besoin si peu a tendance à faciliter le recul et à raisonner sur le comportement global de la base de code entière. Il y a quelque chose là-dedans qui pourrait suggérer que ce ne serait pas une mauvaise idée de se pencher du côté de classes moins nombreuses et plus volumineuses (exerçant toujours une responsabilité sans doute singulière), si cela signifie moins de classes à maintenir et à raisonner, et moins d'interactions tout au long le système.Les interactions
Je dis «interactions» plutôt que «couplage» (bien que réduire les interactions implique de réduire les deux), car vous pouvez utiliser des abstractions pour découpler deux objets concrets, mais ils se parlent toujours. Ils pourraient encore provoquer des effets secondaires dans le processus de cette communication indirecte. Et souvent, je trouve que ma capacité à raisonner sur l'exactitude d'un système est plus liée à ces «interactions» qu'à un «couplage». Minimiser les interactions a tendance à rendre les choses beaucoup plus faciles pour moi de raisonner sur tout, à vol d'oiseau. Cela signifie que les choses ne se parlent pas du tout, et de ce sens, ECS a également tendance à vraiment minimiser les "interactions", et pas seulement le couplage, au minimum le plus strict (au moins, je n'ai pas '
Cela dit, cela pourrait être au moins partiellement moi et mes faiblesses personnelles. J'ai trouvé le plus grand obstacle pour moi de créer des systèmes à grande échelle, et de raisonner en toute confiance, de les parcourir et d'avoir l'impression que je peux apporter les changements souhaités potentiels n'importe où et de manière prévisible, c'est la gestion des états et des ressources ainsi que Effets secondaires. C'est le plus grand obstacle qui commence à surgir lorsque je passe de dizaines de milliers de LOC à des centaines de milliers de LOC à des millions de LOC, même pour du code que j'ai entièrement écrit moi-même. Si quelque chose va me ralentir par-dessus tout, c'est ce sentiment que je ne peux plus comprendre ce qui se passe en termes d'état d'application, de données, d'effets secondaires. Il' Ce n'est pas le temps robotique nécessaire pour effectuer un changement qui me ralentit autant que l'incapacité à comprendre tous les impacts du changement si le système dépasse la capacité de mon esprit à en raisonner. Et réduire les interactions a été, pour moi, le moyen le plus efficace de permettre au produit de grandir beaucoup plus avec beaucoup plus de fonctionnalités sans que je ne sois personnellement submergé par ces choses, car réduire les interactions au minimum réduit également le nombre d'endroits qui peuvent même éventuellement modifier l'état d'application et provoquer des effets secondaires substantiels.
Il peut tourner quelque chose comme ça (où tout dans le diagramme a des fonctionnalités, et évidemment un scénario du monde réel aurait beaucoup, plusieurs fois le nombre d'objets, et c'est un diagramme "d'interaction", pas un couplage, comme un couplage on aurait des abstractions entre les deux):
... à cela où seuls les systèmes ont des fonctionnalités (les composants bleus ne sont plus que des données, et maintenant c'est un schéma de couplage):
Et il y a des réflexions qui émergent sur tout cela et peut-être un moyen d'encadrer certains de ces avantages dans un contexte de POO plus conforme qui est plus compatible avec SOLID, mais je n'ai pas encore tout à fait trouvé les conceptions et les mots, et je le trouve difficile car la terminologie que j'avais l'habitude de lancer concernait directement la POO. Je continue d'essayer de le comprendre en lisant les réponses des gens ici et en faisant de mon mieux pour formuler les miennes, mais il y a des choses très intéressantes sur la nature d'un ECS que je n'ai pas réussi à placer parfaitement mon doigt dessus pourrait être plus largement applicable même aux architectures qui ne l'utilisent pas. J'espère également que cette réponse ne se présente pas comme une promotion ECS! Je trouve cela très intéressant car la conception d'un ECS a vraiment changé radicalement mes pensées,
la source
Les principes SOLIDES sont bons, mais KISS et YAGNI sont prioritaires en cas de conflits. Le but de SOLID est de gérer la complexité, mais si appliquer SOLID lui-même rend le code plus complexe, il va à l'encontre du but. Pour prendre un exemple, si vous avez un petit programme avec seulement quelques classes, l'application de DI, la ségrégation d'interface, etc. pourrait très bien augmenter la complexité globale du programme.
Le principe ouvert / fermé est particulièrement controversé. Il dit essentiellement que vous devez ajouter des fonctionnalités à une classe en créant une sous-classe ou une implémentation distincte de la même interface, mais laisser la classe d'origine intacte. Si vous gérez une bibliothèque ou un service utilisé par des clients non coordonnés, cela a du sens, car vous ne voulez pas casser le comportement sur lequel le client sortant peut compter. Cependant, si vous modifiez une classe qui n'est utilisée que dans votre propre application, il serait souvent beaucoup plus simple et plus propre de simplement modifier la classe.
la source
Dans mon expérience:-
Les grandes classes sont exponentiellement plus difficiles à maintenir à mesure qu'elles grandissent.
Les petites classes sont plus faciles à maintenir et la difficulté de maintenir une collection de petites classes augmente arithmétiquement au nombre de classes.
Parfois, de grandes classes sont inévitables, un gros problème a parfois besoin d'une grande classe, mais elles sont beaucoup plus difficiles à lire et à comprendre. Pour les classes plus petites bien nommées, le nom peut suffire à la compréhension, vous n'avez même pas besoin de regarder le code, sauf s'il y a un problème très spécifique avec la classe.
la source
J'ai toujours du mal à tracer la ligne entre le «S» du solide et le besoin (peut-être à l'ancienne) de laisser un objet avoir toute la connaissance de lui-même.
Il y a 15 ans, j'ai travaillé avec des développeurs Deplhi qui avaient leur Saint Graal pour avoir des cours qui contenaient tout. Donc, pour une hypothèque, vous pouvez ajouter des assurances et des paiements et des revenus et des prêts et des informations fiscales et vous pouvez calculer l'impôt à payer, l'impôt à déduire et il peut se sérialiser, persister sur le disque, etc. etc.
Et maintenant, j'ai le même objet divisé en beaucoup, beaucoup de classes plus petites qui ont l'avantage de pouvoir être testées unitaire beaucoup plus facilement et mieux, mais ne donnent pas un bon aperçu de l'ensemble du modèle d'entreprise.
Et oui, je sais que vous ne voulez pas lier vos objets à la base de données, mais vous pouvez injecter un référentiel dans la grande classe hypothécaire pour résoudre ce problème.
En plus de cela, il n'est pas toujours facile de trouver de bons noms pour les classes qui ne font qu'une petite chose.
Je ne suis donc pas sûr que le «S» soit toujours une bonne idée.
la source