Pour moi, c'est un problème de couplage et lié à la granularité de la conception. Même la forme de couplage la plus lâche introduit des dépendances d'une chose à une autre. Si cela est fait pour des centaines à des milliers d'objets, même s'ils sont tous relativement simples, adhérez à SRP, et même si toutes les dépendances se dirigent vers des abstractions stables, cela donne une base de code qui est très difficile à raisonner comme un ensemble interdépendant.
Il y a des choses pratiques qui vous aident à évaluer la complexité d'une base de code, qui ne sont pas souvent discutées dans SE théorique, comme la profondeur dans la pile d'appels que vous pouvez obtenir avant d'atteindre la fin, et la profondeur que vous devez parcourir avant de pouvoir, avec une grande confiance, comprendre tous les effets secondaires possibles qui pourraient se produire à ce niveau de la pile d'appels, y compris en cas d'exception.
Et j'ai découvert, juste d'après mon expérience, que les systèmes plus plats avec des piles d'appels moins profondes ont tendance à être beaucoup plus faciles à raisonner. Un exemple extrême serait un système entité-composant où les composants ne sont que des données brutes. Seuls les systèmes ont une fonctionnalité, et dans le processus de mise en œuvre et d'utilisation d'un ECS, je l'ai trouvé le système le plus simple jamais, de loin, pour raisonner quand des bases de code complexes qui s'étendent sur des centaines de milliers de lignes de code se résument à quelques dizaines de systèmes qui contiennent toutes les fonctionnalités.
Trop de choses offrent des fonctionnalités
L'alternative avant quand je travaillais dans des bases de code précédentes était un système avec des centaines à des milliers d'objets pour la plupart minuscules, par opposition à quelques dizaines de systèmes volumineux avec certains objets utilisés juste pour passer des messages d'un objet à un autre ( Message
objet, par exemple, qui avait son propre interface publique). C'est essentiellement ce que vous obtenez de manière analogique lorsque vous ramenez l'ECS à un point où les composants ont des fonctionnalités et chaque combinaison unique de composants dans une entité donne son propre type d'objet. Et cela aura tendance à produire des fonctions plus petites et plus simples héritées et fournies par des combinaisons infinies d'objets qui modélisent des idées minuscules ( Particle
objet vsPhysics System
, par exemple). Cependant, cela tend également à produire un graphique complexe d'interdépendances qui rend difficile de raisonner sur ce qui se passe au niveau général, simplement parce qu'il y a tellement de choses dans la base de code qui peuvent réellement faire quelque chose et donc faire quelque chose de mal - - types qui ne sont pas des types "données", mais des types "objets" avec des fonctionnalités associées. Les types qui servent de données pures sans fonctionnalité associée ne peuvent pas mal tourner car ils ne peuvent rien faire par eux-mêmes.
Les interfaces pures n'aident pas beaucoup ce problème de compréhensibilité car même si cela rend les "dépendances au moment de la compilation" moins compliquées et offre plus de marge de manœuvre pour le changement et l'expansion, cela ne rend pas les "dépendances à l'exécution" et les interactions moins compliquées. L'objet client finit toujours par invoquer des fonctions sur un objet de compte concret même si elles sont appelées via IAccount
. Le polymorphisme et les interfaces abstraites ont leur utilité, mais ils ne découplent pas les choses de la manière qui vous aide vraiment à raisonner sur tous les effets secondaires qui pourraient se produire à un moment donné. Pour obtenir ce type de découplage efficace, vous avez besoin d'une base de code qui contient beaucoup moins d'éléments contenant des fonctionnalités.
Plus de données, moins de fonctionnalités
J'ai donc trouvé l'approche ECS, même si vous ne l'appliquez pas complètement, extrêmement utile, car elle transforme ce qui aurait été des centaines d'objets en données brutes avec des systèmes volumineux, plus grossièrement conçus, qui fournissent tous les Fonctionnalité. Il maximise le nombre de types de "données" et minimise le nombre de types "d'objets", et donc minimise absolument le nombre de places dans votre système qui peuvent réellement mal tourner. Le résultat final est un système très "plat" sans graphe complexe de dépendances, juste des systèmes aux composants, jamais l'inverse, et jamais des composants aux autres composants. Ce sont fondamentalement beaucoup plus de données brutes et beaucoup moins d'abstractions, ce qui a pour effet de centraliser et d'aplatir les fonctionnalités de la base de code dans les zones clés, les abstractions clés.
30 choses plus simples ne sont pas nécessairement plus simples à raisonner qu'environ 1 chose plus complexe, si ces 30 choses plus simples sont liées entre elles alors que la chose complexe se suffit à elle-même. Donc, ma suggestion est en fait de transférer la complexité loin des interactions entre les objets et plus vers des objets plus volumineux qui n'ont pas à interagir avec quoi que ce soit d'autre pour réaliser un découplage de masse, à des "systèmes" entiers (pas des monolithes et des objets divins, pensez-vous, et pas des classes avec 200 méthodes, mais quelque chose de bien supérieur à a Message
ou a Particle
malgré une interface minimaliste). Et privilégiez les anciens types de données plus simples. Plus vous en dépendez, moins vous obtiendrez de couplage. Même si cela contredit certaines idées SE, j'ai trouvé que cela aide vraiment beaucoup.