Un bon développeur avec qui je travaille m'a récemment parlé des difficultés rencontrées pour implémenter une fonctionnalité dans un code dont nous avions hérité; il a dit que le problème était que le code était difficile à suivre. De cela, j'ai regardé plus profondément le produit et j'ai réalisé à quel point il était difficile de voir le chemin du code.
Il utilisait tellement d'interfaces et de couches abstraites, qu'essayer de comprendre où les choses commençaient et se terminaient était assez difficile. Cela m'a fait penser aux fois où j'ai regardé des projets antérieurs (avant que je sois si conscient des principes de code propre) et j'ai trouvé qu'il était extrêmement difficile de se déplacer dans le projet, principalement parce que mes outils de navigation dans le code m'arrivaient toujours à une interface. Il faudrait beaucoup d'efforts supplémentaires pour trouver l'implémentation concrète ou où quelque chose était câblé dans une architecture de type plugin.
Je sais que certains développeurs refusent strictement les conteneurs d'injection de dépendance pour cette raison. Cela embrouille tellement le chemin du logiciel que la difficulté de navigation dans le code augmente de façon exponentielle.
Ma question est la suivante: lorsqu'un cadre ou un modèle introduit autant de frais généraux comme celui-ci, cela en vaut-il la peine? Est-ce le symptôme d'un modèle mal mis en œuvre?
Je suppose qu'un développeur devrait se tourner vers le tableau d'ensemble de ce que ces abstractions apportent au projet pour les aider à surmonter la frustration. Cependant, il est généralement difficile de leur faire voir la situation dans son ensemble. Je sais que je n'ai pas réussi à vendre les besoins d'IOC et DI avec TDD. Pour ces développeurs, l'utilisation de ces outils limite beaucoup trop la lisibilité du code.
la source
Eh bien, pas assez d'abstraction et votre code est difficile à comprendre car vous ne pouvez pas isoler quelles parties font quoi.
Trop d'abstraction et vous voyez l'abstraction mais pas le code lui-même, et il devient alors difficile de suivre le fil d'exécution réel.
Pour obtenir une bonne abstraction, il faut BAISER: voir ma réponse à ces questions pour savoir quoi suivre pour éviter ce genre de problèmes .
Je pense qu'éviter une hiérarchie et des noms profonds est le point le plus important à considérer pour le cas que vous décrivez. Si les abstractions étaient bien nommées, vous n'auriez pas à aller trop loin, seulement au niveau de l'abstraction où vous devez comprendre ce qui se passe. La dénomination vous permet d'identifier où se trouve ce niveau d'abstraction.
Le problème survient dans le code de bas niveau, lorsque vous avez vraiment besoin de comprendre tout le processus. Ensuite, l'encapsulation via des modules clairement isolés est la seule aide.
la source
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 aParticle
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.la source
C'est peut-être un symptôme de choisir le mauvais langage de programmation.
la source
Une mauvaise compréhension des modèles de conception a tendance à être une cause majeure de ce problème. L'une des pires que j'ai vues pour ce yo-yo et rebondir d'une interface à une autre sans beaucoup de données concrètes entre les deux était une extension pour Oracle's Grid Control.
Il semblait honnêtement que quelqu'un avait eu une méthode d'usine abstraite et un orgasme de modèle de décorateur partout dans mon code Java. Et ça m'a laissé un sentiment aussi creux et seul.
la source
Je voudrais également mettre en garde contre l'utilisation de fonctionnalités IDE qui facilitent l'abstrait des éléments.
la source