Les abstractions doivent-elles réduire la lisibilité du code?

19

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.

Martin Blore
la source

Réponses:

17

C'est vraiment plus un long commentaire sur la réponse de @kevin cline.

Même si les langues elles-mêmes ne provoquent pas ou n'empêchent pas nécessairement cela, je pense qu'il y a quelque chose dans sa notion qu'il est lié aux langues (ou au moins aux communautés linguistiques) dans une certaine mesure de toute façon. En particulier, même si vous pouvez rencontrer le même problème dans différentes langues, il prendra souvent des formes plutôt différentes dans différentes langues.

Par exemple, lorsque vous rencontrez cela en C ++, il est probable que c'est moins le résultat d'une trop grande abstraction, et plus le résultat d'une trop grande intelligence. Par exemple, le programmeur a caché la transformation cruciale qui se produit (que vous ne pouvez pas trouver) dans un itérateur spécial, donc ce qui ressemble à la copie de données d'un endroit à un autre a vraiment un certain nombre d'effets secondaires qui n'ont rien à faire avec cette copie des données. Juste pour garder les choses intéressantes, cela est entrelacé avec une sortie créée comme effet secondaire de la création d'un objet temporaire au cours de la conversion d'un type d'objet en un autre.

En revanche, lorsque vous le rencontrez en Java, vous êtes beaucoup plus susceptible de voir une variante du "monde bonjour de l'entreprise" bien connu, où au lieu d'une seule classe triviale qui fait quelque chose de simple, vous obtenez une classe de base abstraite et une classe dérivée concrète qui implémente l'interface X, et est créée par une classe d'usine dans un cadre DI, etc. Les 10 lignes de code qui font le vrai travail sont enfouies sous 5000 lignes d'infrastructure.

Une partie dépend de l'environnement au moins autant que de la langue - travailler directement avec des environnements de fenêtrage comme X11 et MS Windows est connu pour transformer un programme trivial "hello world" en plus de 300 lignes de déchets presque indéchiffrables. Au fil du temps, nous avons développé diverses boîtes à outils pour nous en isoler également - mais 1) ces boîtes à outils sont elles-mêmes assez banales et 2) le résultat final est toujours non seulement plus grand et plus complexe, mais aussi généralement moins flexible qu'un équivalent en mode texte (par exemple, même s'il ne s'agit que d'imprimer du texte, le rediriger vers un fichier est rarement possible / pris en charge).

Pour répondre (au moins en partie) à la question initiale: du moins quand je l'ai vue, il s'agissait moins d'une mauvaise mise en œuvre d'un modèle que de simplement appliquer un modèle qui n'était pas adapté à la tâche à accomplir - la plupart souvent d'essayer d'appliquer un modèle qui pourrait bien être utile dans un programme qui est inévitablement énorme et complexe, mais lorsqu'il est appliqué à un problème plus petit finit par le rendre aussi énorme et complexe, même si dans ce cas la taille et la complexité étaient vraiment évitables .

Jerry Coffin
la source
7

Je trouve que cela est souvent causé par le fait de ne pas adopter une approche YAGNI . Tout ce qui passe par des interfaces, même s'il n'y a qu'une seule implémentation concrète et aucun plan actuel pour en introduire d'autres, est un excellent exemple d'ajout de complexité dont vous n'aurez pas besoin. C'est probablement une hérésie, mais je ressens la même chose pour beaucoup d'utilisation de l'injection de dépendance.

Carson63000
la source
+1 pour mentionner YAGNI et les abstractions avec des points de référence uniques. Le rôle principal de faire une abstraction est de prendre en compte le point commun de plusieurs choses. Si une abstraction n'est référencée qu'à partir d'un seul point, nous ne pouvons pas parler de factorisation de choses communes, une abstraction comme celle-ci ne fait que contribuer au problème du yoyo. Je voudrais étendre cela parce que cela est vrai pour toutes sortes d'abstractions: fonctions, génériques, macros, peu importe ...
Calmarius
3

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.

Klaim
la source
3
Eh bien, pas assez d'abstraction et votre code est difficile à comprendre car vous ne pouvez pas isoler quelles parties font quoi. C'est de l'encapsulation, pas de l'abstraction. Vous pouvez isoler des pièces dans des classes concrètes sans trop d'abstraction.
Déclaration du
Les classes ne sont pas les seules abstractions que nous utilisons: fonctions, modules / bibliothèques, services, etc. Dans vos classes, vous résumez généralement chaque fonctionnalité derrière une fonction / méthode, qui peut appeler une autre méthode qui se résume mutuellement.
Klaim
1
@Statement: l'encapsulation de données est bien sûr une abstraction.
Ed S.
Les hiérarchies des espaces de noms sont vraiment sympas, cependant.
JAB
2

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 ( Messageobjet, 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 ( Particleobjet 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 Messageou a Particlemalgré 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
0

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?

C'est peut-être un symptôme de choisir le mauvais langage de programmation.

Kevin Cline
la source
1
Je ne vois pas comment cela a quelque chose à voir avec la langue de choix. Les abstractions sont un concept de haut niveau indépendant du langage.
Ed S.
@Ed: Certaines abstractions sont plus simplement réalisables dans certaines langues que dans d'autres.
kevin cline
Oui, mais cela ne signifie pas que vous ne pouvez pas écrire une abstraction parfaitement maintenable et facilement compréhensible dans ces langues. Mon point était que votre réponse ne répond pas à la question ou n'aide en aucun cas le PO.
Ed S.
0

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.

Jeff Langemeier
la source
-1

Je voudrais également mettre en garde contre l'utilisation de fonctionnalités IDE qui facilitent l'abstrait des éléments.

Christopher Mahan
la source