Est-il possible d'appliquer SEC sans augmenter le couplage?

14

Supposons que nous ayons un module logiciel A qui implémente une fonction F. Un autre module B implémente la même fonction que F '.

Il existe plusieurs façons de se débarrasser du code en double:

  1. Soit A utiliser F 'de B.
  2. Soit B utiliser F de A.
  3. Mettez F dans son propre module C et laissez A et B l'utiliser.

Toutes ces options génèrent des dépendances supplémentaires entre les modules. Ils appliquent le principe DRY au prix d'augmenter le couplage.

Pour autant que je puisse voir, le couplage est toujours augmenté ou, à la location, déplacé à un niveau supérieur lors de l'application de SEC. Il semble y avoir un conflit entre deux des principes les plus élémentaires de la conception de logiciels.

(En fait, je ne trouve pas surprenant qu'il y ait de tels conflits. C'est probablement ce qui rend la conception d'un logiciel si difficile. Je trouve surprenant que ces conflits ne soient normalement pas abordés dans les textes d'introduction.)

Edit (pour clarification): Je suppose que l'égalité de F et F 'n'est pas seulement une coïncidence. Si F devra être modifié, F 'devra probablement être modifié de la même manière.

Frank Puffer
la source
2
... Je pense que DRY peut être une stratégie très utile, mais cette question illustre une inefficacité avec DRY. Certains (par exemple, les amateurs de POO) pourraient faire valoir que vous devriez copier / coller F dans B juste pour maintenir l'autonomie conceptuelle de A et B mais je n'ai pas rencontré de scénario où je ferais cela. Je pense que copier / coller du code est à peu près la pire option, je ne peux pas supporter ce sentiment de "perte de mémoire à court terme" où j'étais tellement sûr d'avoir déjà écrit une méthode / fonction pour faire quelque chose; corriger un bogue dans une fonction et oublier d'en mettre à jour une autre peut être un autre problème majeur.
2018
3
Il y a beaucoup de ces principes OO qui se contredisent. Dans la plupart des cas, vous devez trouver un compromis raisonnable. Mais à mon humble avis, le principe SEC est le plus précieux. Comme l'a écrit @jrh: avoir le même comportement implémenté à plusieurs endroits est un cauchemar de maintenance qui devrait être évité à tout prix. Découvrir que vous avez oublié de mettre à jour l'une des copies redondantes en production peut mettre votre entreprise en panne.
Timothy Truckle
2
@TimothyTruckle: Nous ne devrions pas les appeler principes OO car ils s'appliquent également à d'autres paradigmes de programmation. Et oui, SEC est précieux mais aussi dangereux s'il est exagéré. Non seulement parce qu'elle tend à créer des dépendances et donc de la complexité. Elle est également souvent appliquée aux doublons provoqués par coïncidence qui ont différentes raisons de changer.
Frank Puffer
1
... parfois aussi quand je suis tombé sur ce scénario, j'ai pu décomposer F en parties qui peuvent être utilisables pour ce dont A a besoin et ce dont B a besoin.
HJR
1
Le couplage n'est pas intrinsèquement une mauvaise chose et est souvent nécessaire pour réduire les erreurs et augmenter la productivité. Si vous utilisiez une fonction parseInt de la bibliothèque standard de votre langue dans votre fonction, vous coupleriez votre fonction à la bibliothèque standard. Je n'ai pas vu un programme qui ne fait pas cela depuis de nombreuses années. La clé est de ne pas créer de couplages inutiles. Le plus souvent, une interface est utilisée pour éviter / supprimer un tel couplage. Par exemple, ma fonction peut accepter une implémentation de parseInt comme argument. Cependant, ce n'est pas toujours nécessaire, ni toujours sage.
Joshua Jones

Réponses:

14

Toutes ces options génèrent des dépendances supplémentaires entre les modules. Ils appliquent le principe DRY au prix d'augmenter le couplage.

Pourquoi oui ils le font. Mais ils diminuent le couplage entre les lignes. Ce que vous obtenez, c'est le pouvoir de changer le couplage. Le couplage se présente sous plusieurs formes. L'extraction de code augmente l'indirection et l'abstraction. Augmenter cela peut être bon ou mauvais. La première chose qui décide de ce que vous obtenez est le nom que vous utilisez pour cela. Si regarder le nom me laisse surpris quand je regarde à l'intérieur, vous n'avez rendu service à personne.

Aussi, ne suivez pas SEC dans le vide. Si vous tuez la duplication, vous prenez la responsabilité de prévoir que ces deux utilisations de ce code changeront ensemble. S'ils sont susceptibles de changer indépendamment, vous avez causé de la confusion et du travail supplémentaire pour peu d'avantages. Mais un très bon nom peut rendre cela plus agréable au goût. Si tout ce à quoi vous pouvez penser est une mauvaise réputation, alors arrêtez-vous maintenant.

Le couplage existera toujours à moins que votre système ne soit si isolé que personne ne saura jamais s'il fonctionne. Le refactoring du couplage consiste donc à choisir son poison. Suivre DRY peut être payant en minimisant le couplage créé en exprimant la même décision de conception à plusieurs reprises jusqu'à ce qu'il soit très difficile de changer. Mais DRY peut rendre impossible la compréhension de votre code. La meilleure façon de sauver cette situation est de trouver un très bon nom. Si vous ne pouvez pas penser à un bon nom, j'espère que vous êtes habile à éviter les noms dénués de sens

candied_orange
la source
Juste pour m'assurer de bien comprendre votre point, permettez-moi de le dire un peu différemment: si vous attribuez un bon nom au code extrait, le code extrait n'est plus pertinent pour comprendre le logiciel parce que le nom dit tout (idéalement). Le couplage existe toujours au niveau technique mais pas au niveau cognitif. Par conséquent, il est relativement inoffensif, non?
Frank Puffer
1
Peu importe
Basilevs
@FrankPuffer mieux?
candied_orange
3

Il existe des moyens de rompre les dépendances explicites. Une méthode courante consiste à injecter des dépendances dans l'exécution. De cette façon, vous obtenez SEC, retirez l'accouplement au prix d'une sécurité statique. C'est tellement populaire de nos jours, que les gens ne comprennent même pas, que c'est un compromis. Par exemple, les conteneurs d'applications fournissent régulièrement une gestion des dépendances qui complique énormément les logiciels en masquant la complexité. Même une injection de constructeur simple ne parvient pas à garantir certains contrats en raison d'un système de type manquant.

Pour répondre au titre - oui, c'est possible, mais préparez-vous aux conséquences de l'envoi du runtime.

  • Définir l'interface F A dans A, fournissant la fonctionnalité de F
  • Définir l'interface F B en B
  • Mettez F en C
  • Créez le module D pour gérer toutes les dépendances (cela dépend de A, B et C)
  • Adapter F à F A et F B en D
  • Injecter (passer) les enveloppes à A et B

De cette façon, le seul type de dépendances que vous auriez serait D en fonction de chaque autre module.

Ou enregistrez C dans le conteneur d'application avec une injection de dépendances intégrée et profitez de la possibilité de câbler automatiquement des boucles et des blocages de chargement de classe à croissance lente.

Basilevs
la source
1
+1, mais avec la mise en garde explicite que c'est généralement un mauvais compromis. Cette sécurité statique est là pour votre protection, et le contourner avec un tas d'actions fantasmagoriques à distance demande simplement des bogues difficiles à localiser plus loin lorsque la complexité de votre projet augmente un peu ...
Mason Wheeler
1
Peut-on vraiment dire que DI brise la dépendance? Même formellement, vous avez besoin d'une interface avec la signature de F pour l'implémenter. Mais quand même, si le module A utilise généralement F à partir de C, il y dépend, indépendamment du fait que C soit injecté au moment de l'exécution ou directement lié.DI ne rompt pas la dépendance, le bug ne reporte l'échec que si la dépendance n'est pas fournie
max630
@ max630, il remplace la dépendance d'implémentation par une dépendance contractuelle, qui est plus faible.
Basilevs
@ max630 Vous avez raison. On ne peut pas dire que DI brise une dépendance. DI est, en fait, une méthode d' introduction de dépendances, et est vraiment orthogonale à la question posée à propos du couplage. Un changement dans F (ou l'interface l'encapsulant) nécessiterait toujours un changement dans A et B.
king-side-slide
1

Je ne suis pas sûr qu'une réponse sans autre contexte ait du sens.

Dépend Adéjà Bou vice versa? - dans ce cas, nous pourrions avoir un choix évident de maison pour F.

Partagez-vous Aet Bpartagez-vous déjà des dépendances communes qui pourraient être un bon foyer F?

Quelle est la taille / la complexité F? De quoi d'autre Fdépend-il?

Sont des modules Aet Butilisés dans le même projet?

Va A- t-il et Bfinira- t- il par partager une dépendance commune?

Quel système de langue / module est utilisé: Dans quelle mesure un nouveau module est-il pénible pour le programmeur, en termes de performances? Par exemple, si vous écrivez en C / C ++ avec le système de modules COM, ce qui cause des problèmes au code source, nécessite des outils alternatifs, a des implications sur le débogage et a des implications en termes de performances (pour les appels inter-modules), je pourrais prenez une pause sérieuse.

D'un autre côté, si vous parlez de DLL Java ou C # qui se combinent de manière plutôt transparente dans un environnement d'exécution unique, c'est une autre question.


Une fonction est une abstraction et prend en charge DRY.

Cependant, de bonnes abstractions doivent être complètes - des abstractions incomplètes peuvent très bien amener le client consommateur (programmeur) à combler le déficit en utilisant la connaissance de l'implémentation sous-jacente: cela se traduit par un couplage plus étroit que si l'abstraction était proposée à la place comme plus complète.

Donc, je dirais de chercher à créer une meilleure abstraction Aet Bà dépendre que de simplement déplacer une seule fonction dans un nouveau module C

Je chercherais un ensemble de fonctions pour taquiner une nouvelle abstraction, c'est-à-dire que je pourrais attendre que la base de code soit plus avancée afin d'identifier une refactorisation d'abstraction plus complète / plus complète à faire plutôt qu'une basée sur un seul code de fonction dire.

Erik Eidt
la source
2
N'est-il pas dangereux de coupler abstractions et graphe de dépendance?
Basilevs
Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.Cela suppose que A dépendra toujours de B (ou vice versa), ce qui est une hypothèse très dangereuse. Le fait que OP considère F comme ne faisant pas intrinsèquement partie de A (ou B) suggère que F existe sans être inhérent à l'une ou l'autre bibliothèque. Si F appartient à une bibliothèque (par exemple une méthode d'extension DbContext (F) et une bibliothèque d'encapsulation Entity Framework (A ou B)), la question de OP serait théorique.
Flater
0

Les réponses ici qui se concentrent sur toutes les façons dont vous pouvez «minimiser» ce problème ne vous rendent pas service. Et les «solutions» offrant simplement différentes façons de créer un couplage ne sont pas vraiment des solutions.

La vérité est que vous ne comprenez pas le problème même que vous avez créé. Le problème avec votre exemple n'a rien à voir avec DRY, plutôt (plus largement) avec la conception d'application.

Demandez-vous pourquoi les modules A et B sont séparés s'ils dépendent tous deux de la même fonction F? Bien sûr, vous aurez des problèmes avec la gestion des dépendances / abstraction / couplage / you-name-it si vous vous engagez à une mauvaise conception.

Une modélisation d'application appropriée est effectuée en fonction du comportement. En tant que tel, les morceaux de A et B qui dépendent de F doivent être extraits dans leur propre module indépendant. Si cela n'est pas possible, alors A et B doivent être combinés. Dans les deux cas, A et B ne sont plus utiles au système et devraient cesser d'exister.

DRY est un principe qui peut être utilisé pour exposer une mauvaise conception, pas la provoquer. Si vous ne pouvez pas atteindre DRY ( quand cela s'applique vraiment - en notant votre modification) en raison de la structure de votre application, c'est un signe clair que la structure est devenue un handicap. C'est pourquoi le «refactoring continu» est également un principe à suivre.

Les ABC des autres principes de conception (SOLID, DRY, etc.) sont tous utilisés pour rendre la modification (y compris la refactorisation) d'une application plus indolore. Concentrez -vous sur ce que , et tous les autres problèmes commencent à disparaître.

glissière côté roi
la source
Suggérez-vous d'avoir exactement un module dans chaque application?
Basilevs
@Basilevs Absolument pas (sauf si cela est garanti). Je suggère d'avoir autant de modules complètement découplés que possible. Après tout, c'est tout le but d'un module.
king-side-slide
Eh bien, la question implique que les modules A et B contiennent des fonctions indépendantes, et d'être déjà extraits en conséquence, pourquoi et comment devraient-ils cesser d'exister? Quelle est votre solution au problème comme indiqué?
Basilevs
@Basilevs La question implique que A et B n'ont pas été modélisés correctement. C'est cette carence inhérente qui est à l'origine du problème. Certes , le simple fait qu'ils ne exist est pas la preuve qu'ils devraient exister. C'est le point que je fais valoir ci-dessus. De toute évidence, une conception alternative est nécessaire pour éviter de casser SEC. Il est important de comprendre que le but même de tous ces «principes de conception» est de faciliter la modification d'une application.
king-side-slide
Une tonne d'autres méthodes, avec des dépendances complètement différentes, ont-elles été mal modélisées et doivent être refaites, simplement à cause de cette seule méthode non accidentelle? Ou supposez-vous que les modules A et B contiennent chacun une seule méthode?
Basilevs
0

Toutes ces options génèrent des dépendances supplémentaires entre les modules. Ils appliquent le principe DRY au prix d'augmenter le couplage.

J'ai une opinion différente, au moins pour la troisième option:

D'après votre description:

  • A a besoin de F
  • B a besoin de F
  • Ni A ni B n'ont besoin l'un de l'autre.

Mettre F dans un module C n'augmente pas le couplage car A et B ont déjà besoin de la fonctionnalité de C.

mouviciel
la source