Le couplage lâche sans cas d'utilisation est-il un anti-modèle?

22

Le couplage lâche est, pour certains développeurs, le Saint Graal d'un logiciel bien conçu. C'est certainement une bonne chose lorsqu'il rend le code plus flexible face aux changements susceptibles de se produire dans un avenir prévisible, ou évite la duplication de code.

D'un autre côté, les efforts de couplage lâche des composants augmentent la quantité d'indirection dans un programme, augmentant ainsi sa complexité, ce qui le rend souvent plus difficile à comprendre et le rend souvent moins efficace.

Considérez-vous que mettre l'accent sur le couplage lâche sans aucun cas d'utilisation pour le couplage lâche (comme éviter la duplication de code ou planifier des changements susceptibles de se produire dans un avenir prévisible) est un anti-modèle? Le couplage lâche peut-il tomber sous l'égide de YAGNI?

dsimcha
la source
1
La plupart des avantages ont un coût. À utiliser si l'avantage l'emporte sur le coût.
LennyProgrammers
4
vous avez oublié le revers de la médaille de couplage lâche, et c'est l' high cohesionun sans l'autre est un gaspillage d'efforts et des illustrations manque fondamental de compréhension de l'un ou l'autre.

Réponses:

13

Quelque peu.

Parfois, un couplage lâche qui vient sans trop d'effort est très bien, même si vous n'avez pas d'exigences spécifiques exigeant que certains modules soient découplés. Le fruit bas, pour ainsi dire.

D'un autre côté, une ingénierie excessive pour des changements ridicules nécessitera beaucoup de complexité et d'efforts inutiles. YAGNI, comme vous dites, le frappe sur la tête.

Fishtoaster
la source
22

La pratique de programmation X est-elle bonne ou mauvaise? De toute évidence, la réponse est toujours «cela dépend».

Si vous regardez votre code, vous vous demandez quels "modèles" vous pouvez injecter, alors vous le faites mal.

Si vous construisez votre logiciel de manière à ce que les objets non liés ne jouent pas les uns avec les autres, vous le faites correctement.

Si vous "concevez" votre solution afin qu'elle puisse être étendue et modifiée à l'infini, vous la compliquez en fait.

Je pense qu'à la fin de la journée, vous vous retrouvez avec la seule vérité: est-ce plus ou moins compliqué de découpler les objets? S'il est moins compliqué de les coupler, alors c'est la bonne solution. S'il est moins compliqué de les découpler, alors c'est la bonne solution.

(Je travaille actuellement dans une base de code assez petite qui fait un travail simple d'une manière très compliquée, et une partie de ce qui le rend si compliqué est le manque de compréhension des termes "couplage" et "cohésion" de la part de l'original développeurs.)

dash-tom-bang
la source
4
Je l'ai fait l'autre jour: j'ai pensé que j'aurais besoin d'une indirection entre deux classes "au cas où". Puis je me suis fait mal à la tête en essayant de déboguer quelque chose, j'ai arraché l'indirection et j'étais beaucoup plus heureux.
Frank Shearar
3

Je pense que ce que vous voulez dire ici, c'est le concept de cohésion . Ce code a-t-il un bon objectif? Puis-je intérioriser cet objectif et comprendre la «vue d'ensemble» de ce qui se passe?

Cela pourrait conduire à un code difficile à suivre, non seulement parce qu'il y a beaucoup plus de fichiers source (en supposant que ce sont des classes distinctes), mais parce qu'aucune classe unique ne semble avoir un but.

D'un point de vue agile, je pourrais suggérer qu'un tel couplage lâche serait un anti-modèle. Sans la cohésion, ni même les cas d'utilisation, vous ne pouvez pas écrire de tests unitaires raisonnables et ne pouvez pas vérifier la finalité du code. Désormais, le code agile peut entraîner un couplage lâche, par exemple lorsque le développement piloté par les tests est utilisé. Mais si les bons tests ont été créés, dans le bon ordre, il est probable qu'il y ait à la fois une bonne cohésion et un couplage lâche. Et vous ne parlez que des cas où clairement il n'y a pas de cohésion.

Encore une fois du point de vue agile, vous ne voulez pas de ce niveau artificiel d'indirection car c'est un effort gaspillé sur quelque chose qui ne sera probablement pas nécessaire de toute façon. Il est beaucoup plus facile de refactoriser lorsque le besoin est réel.

Dans l'ensemble, vous voulez un couplage élevé au sein de vos modules et un couplage lâche entre eux. Sans le couplage élevé, vous n'avez probablement pas de cohésion.

Macneil
la source
1

Pour la majorité des questions comme celle-ci, la réponse est «cela dépend». Dans l'ensemble, si je peux faire une conception logiquement reliée de manière logique, sans frais généraux importants, je le ferai. Éviter un couplage inutile dans le code est, à mon avis, un objectif de conception tout à fait valable.

Une fois qu'il arrivera à une situation où il semblerait que les composants devraient logiquement être étroitement couplés, je chercherai un argument convaincant avant de commencer à les casser.

Je suppose que le principe sur lequel je travaille avec la plupart de ces types de pratique est celui de l'inertie. J'ai une idée de la façon dont j'aimerais que mon code fonctionne et si je peux le faire de cette façon sans rendre la vie plus difficile, je le ferai. Si cela rend le développement plus difficile mais la maintenance et les travaux futurs plus faciles, j'essaierai de deviner si ce sera plus de travail pendant la durée de vie du code et l'utiliser comme mon guide. Sinon, il faudrait que ce soit un point de conception délibéré qui mérite d'être suivi.

glénatron
la source
1

La réponse simple est que le couplage lâche est bon lorsqu'il est fait correctement.

Si le principe d'une fonction, d'un but est suivi, il devrait être assez facile de suivre ce qui se passe. En outre, le code de couple lâche suit naturellement sans aucun effort.

Règles de conception simples: 1. ne construisez pas la connaissance de plusieurs éléments en un seul point (comme indiqué partout, cela dépend) à moins que vous ne construisiez une interface de façade. 2. une fonction - un objectif (cet objectif peut avoir plusieurs facettes comme dans une façade) 3. un module - un ensemble clair de fonctions interdépendantes - un objectif clair 4. si vous ne pouvez pas simplement le tester à l'unité, alors il n'a pas un but simple

Tous ces commentaires sur la facilité de refactorisation plus tard sont une charge de morues. Une fois que les connaissances sont intégrées à de nombreux endroits, en particulier dans les systèmes distribués, le coût de la refactorisation, la synchronisation du déploiement et à peu près tous les autres coûts le font tellement oublier que dans la plupart des cas, le système finit par être mis à la poubelle à cause de cela.

La chose triste à propos du développement de logiciels ces jours-ci, c'est que 90% des gens développent de nouveaux systèmes et n'ont aucune capacité à comprendre les anciens systèmes, et ne sont jamais là quand le système a atteint un si mauvais état de santé en raison de la refonte continue des morceaux.

sweetfa
la source
0

Peu importe à quel point une chose est étroitement couplée à une autre si cette autre chose ne change jamais. J'ai trouvé généralement plus productif au fil des ans de se concentrer sur la recherche de moins de raisons pour que les choses changent, pour rechercher la stabilité, que pour les rendre plus faciles à changer en essayant d'obtenir la forme de couplage la plus lâche possible. Le découplage s'est avéré très utile, au point que je préfère parfois une duplication de code modeste pour découpler des packages. Comme exemple de base, j'avais le choix d'utiliser ma bibliothèque de mathématiques pour implémenter une bibliothèque d'images. Je ne l'ai pas fait et j'ai simplement dupliqué certaines fonctions mathématiques de base qui étaient triviales à copier.

Maintenant, ma bibliothèque d'images est complètement indépendante de la bibliothèque de mathématiques d'une manière où, peu importe le type de modifications que j'apporte à ma bibliothèque de mathématiques, cela n'affectera pas la bibliothèque d'images. C'est mettre la stabilité avant tout. La bibliothèque d'images est maintenant plus stable, car elle a considérablement moins de raisons de changer, car elle est dissociée de toute autre bibliothèque qui pourrait changer (à part la bibliothèque standard C qui, espérons-le, ne devrait jamais changer). En prime, il est également facile à déployer quand il s'agit simplement d'une bibliothèque autonome qui ne nécessite pas de tirer un tas d'autres bibliothèques pour la construire et l'utiliser.

La stabilité m'est très utile. J'aime construire une collection de code bien testé qui a de moins en moins de raisons de changer à l'avenir. Ce n'est pas un rêve de pipe; J'ai du code C que j'utilise et utilise depuis la fin des années 80, qui n'a pas changé du tout depuis. Il s'agit certes de trucs de bas niveau comme du code orienté pixel et lié à la géométrie alors que beaucoup de mes trucs de haut niveau sont devenus obsolètes, mais c'est quelque chose qui aide toujours beaucoup à avoir autour. Cela signifie presque toujours une bibliothèque qui repose sur de moins en moins de choses, voire rien d'extérieur. La fiabilité monte et monte si votre logiciel dépend de plus en plus de fondations stables qui trouvent peu ou pas de raisons de changer. Moins de pièces mobiles est vraiment agréable, même si en pratique les pièces mobiles sont beaucoup plus nombreuses que les pièces stables.

Le couplage lâche est dans la même veine, mais je trouve souvent que le couplage lâche est tellement moins stable que pas de couplage. À moins que vous ne travailliez dans une équipe avec des concepteurs d'interface et des clients bien supérieurs qui ne changent pas d'avis que je n'ai jamais travaillé, même les interfaces pures trouvent souvent des raisons de changer de manière à provoquer des ruptures en cascade dans le code. Cette idée selon laquelle la stabilité peut être obtenue en dirigeant les dépendances vers l'abstrait plutôt que vers le concret n'est utile que si la conception de l'interface est plus facile à corriger la première fois que l'implémentation. Je trouve souvent inversé le cas où un développeur aurait pu créer une très bonne, sinon merveilleuse, implémentation compte tenu des exigences de conception qu'il pensait devoir remplir, pour constater à l'avenir que les exigences de conception changeaient complètement.

J'aime donc privilégier la stabilité et le découplage complet pour pouvoir dire au moins en toute confiance: "Cette petite bibliothèque isolée qui a été utilisée pendant des années et sécurisée par des tests approfondis n'a presque aucune probabilité d'exiger des changements quoi qu'il se passe dans le monde extérieur chaotique . " Cela me donne une petite tranche de raison quel que soit le type de changements de conception qui sont nécessaires à l'extérieur.

Couplage et stabilité, exemple ECS

J'adore également les systèmes d'entité-composant et ils introduisent beaucoup de couplage étroit parce que les dépendances du système aux composants accèdent et manipulent directement les données brutes, comme ceci:

entrez la description de l'image ici

Toutes les dépendances ici sont assez étroites puisque les composants exposent simplement les données brutes. Les dépendances ne se dirigent pas vers des abstractions, elles se dirigent vers des données brutes, ce qui signifie que chaque système possède le maximum de connaissances possible sur chaque type de composant auquel il demande d'accéder. Les composants n'ont aucune fonctionnalité avec tous les systèmes accédant et altérant les données brutes. Cependant, il est très facile de raisonner sur un système comme celui-ci car il est si plat. Si une texture apparaît visqueuse, alors vous savez immédiatement avec ce système que seul le système de rendu et de peinture accède aux composants de texture, et vous pouvez probablement rapidement éliminer le système de rendu car il ne lit que des textures conceptuellement.

Pendant ce temps, une alternative faiblement couplée pourrait être la suivante:

entrez la description de l'image ici

... avec toutes les dépendances vers des fonctions abstraites, pas des données, et chaque élément de ce diagramme exposant une interface publique et des fonctionnalités qui lui sont propres. Ici, toutes les dépendances peuvent être très lâches. Les objets peuvent même ne pas dépendre directement les uns des autres et interagir les uns avec les autres via des interfaces pures. Il est toujours très difficile de raisonner sur ce système, surtout si quelque chose se passe mal, étant donné l'enchevêtrement complexe des interactions. Il y aura également plus d'interactions (plus de couplage, quoique plus lâches) que l'ECS car les entités doivent connaître les composants qu'elles agrègent, même si elles ne connaissent que l'interface publique abstraite de l'autre.

De plus, s'il y a des modifications de conception sur quoi que ce soit, vous obtenez plus de ruptures en cascade que l'ECS, et il y aura généralement plus de raisons et de tentations pour les modifications de conception car chaque chose essaie de fournir une interface et une abstraction orientées objet agréables. Cela vient immédiatement avec l'idée que chaque petite chose essaiera d'imposer des contraintes et des limitations à la conception, et ces contraintes sont souvent ce qui justifie des changements de conception. La fonctionnalité est beaucoup plus limitée et doit faire beaucoup plus d'hypothèses de conception que les données brutes.

J'ai trouvé dans la pratique que le type de système ECS "plat" ci-dessus était tellement plus facile à raisonner que même les systèmes les plus lâchement couplés avec une toile d'araignée complexe de dépendances lâches et, plus important pour moi, je trouve si peu de raisons pour que la version ECS ait besoin de changer les composants existants, car les composants dépendants n'ont aucune responsabilité, sauf de fournir les données appropriées nécessaires au fonctionnement des systèmes. Comparez la difficulté de concevoir une IMotioninterface pure et un objet de mouvement concret implémentant cette interface qui fournit des fonctionnalités sophistiquées tout en essayant de maintenir des invariants sur les données privées par rapport à un composant de mouvement qui n'a besoin que de fournir des données brutes pertinentes pour résoudre le problème et ne se soucie pas de fonctionnalité.

La fonctionnalité est tellement plus difficile à obtenir correctement que les données, c'est pourquoi je pense qu'il est souvent préférable de diriger le flux des dépendances vers les données. Après tout, combien de bibliothèques vectorielles / matricielles existe-t-il? Combien d'entre eux utilisent exactement la même représentation des données et ne diffèrent que subtilement par leur fonctionnalité? D'innombrables, et pourtant nous en avons encore tellement malgré des représentations de données identiques car nous voulons de subtiles différences de fonctionnalités. Combien de bibliothèques d'images existe-t-il? Combien d'entre eux représentent des pixels d'une manière différente et unique? Presque aucun, et montrant à nouveau que la fonctionnalité est beaucoup plus instable et sujette aux changements de conception que les données dans de nombreux scénarios. Bien sûr, à un moment donné, nous avons besoin de fonctionnalités, mais vous pouvez concevoir des systèmes où la majeure partie des dépendances se dirigent vers les données, et non vers des abstractions ou des fonctionnalités en général. Ce serait prioriser la stabilité au-dessus du couplage.

Les fonctions les plus stables que j'ai jamais écrites (celles que j'utilise et réutilise depuis la fin des années 80 sans avoir à les changer du tout) étaient toutes celles qui s'appuyaient sur des données brutes, comme une fonction de géométrie qui acceptait simplement un tableau de des flottants et des entiers, pas ceux qui dépendent d'un Meshobjet ou d'une IMeshinterface complexe , ou la multiplication vectorielle / matrice qui dépendait juste de, float[]ou double[]pas celle qui dépendait FancyMatrixObjectWhichWillRequireDesignChangesNextYearAndDeprecateWhatWeUse.


la source