Quand ne pas appliquer le principe d'inversion de dépendance?

43

J'essaie actuellement de comprendre SOLID. Donc, le principe d'inversion de dépendance signifie que deux classes quelconques doivent communiquer via des interfaces, pas directement. Exemple: Si class Aa une méthode qui attend un pointeur sur un objet de type class B, cette méthode devrait en fait attendre un objet de type abstract base class of B. Cela aide aussi pour Open / Close.

À condition d’avoir bien compris cela, ma question est de savoir si c’est une bonne pratique de l’appliquer à toutes les interactions de classe ou devrais-je essayer de penser en termes de couches ?

Je suis sceptique parce que nous payons un prix pour suivre ce principe. Dis, j'ai besoin d'implémenter une fonctionnalité Z. Après analyse, je conclus que cette fonctionnalité Zconsiste en fonctionnalité A, Bet C. Je crée une classe de façadeZ qui, à travers des interfaces, utilise des classes A, Bet C. Je commence à coder la mise en œuvre et à un moment je me rends compte que la tâche Zconsiste en fait de fonctionnalité A, Bet D. Maintenant, je dois supprimer l' Cinterface, le Cprototype de classe et écrire une Dinterface et une classe séparées . Sans interfaces, seule la classe aurait dû être remplacée.

En d'autres termes, pour changer quelque chose, je dois changer 1. l'appelant 2. l'interface 3. la déclaration 4. l'implémentation. Dans un python couplé directement la mise en œuvre, je besoin de changer seulement la mise en œuvre.

Vorac
la source
13
L'inversion de dépendance est simplement une technique, elle ne devrait donc être appliquée qu'en cas de besoin ... il n'y a pas de limite à son application, donc si vous l'appliquez partout, vous vous retrouvez avec des ordures: comme dans toute autre situation. technique spécifique.
Frank Hileman
Pour le dire simplement, l’application de certains principes de conception de logiciels dépend de la possibilité de procéder à une refactorisation sans merci lorsque les exigences changent. Parmi ceux-ci, on pense que la partie interface capture le mieux les invariants contractuels de la conception, alors que le code source (implémentation) devrait tolérer des modifications plus fréquentes.
Rwong
@rwong Une interface capture les invariants contractuels uniquement si vous utilisez un langage prenant en charge les invariants contractuels. Dans les langages courants (Java, C #), une interface est simplement un ensemble de signatures API. L'ajout d'interfaces superflues ne fait que dégrader une conception.
Frank Hileman
Je dirais que vous avez mal compris. DIP consiste à éviter les dépendances au moment de la compilation d’un composant "de haut niveau" à un autre "de bas niveau", afin de permettre la réutilisation du composant de haut niveau dans d’autres contextes, dans lesquels vous utiliseriez une implémentation différente pour les versions basses. composant de niveau; cela est fait en créant un type abstrait au niveau supérieur, qui est implémenté par les composants de bas niveau; de sorte que les composants haut et bas dépendent de cette abstraction. En fin de compte, les composants de niveau inférieur et supérieur communiquent via une interface, mais ce n'est pas l'essence du DIP.
Rogério

Réponses:

87

Dans de nombreux dessins animés ou d'autres médias, les forces du bien et du mal sont souvent illustrées par un ange et un démon assis sur les épaules du personnage. Dans notre récit ici, au lieu de bien et de mal, nous avons SOLID sur une épaule et YAGNI (vous n'en aurez pas besoin!) Assis sur l'autre.

Les principes SOLID utilisés au mieux conviennent parfaitement aux systèmes d'entreprise gigantesques, complexes et ultra-configurables. Pour les systèmes plus petits ou plus spécifiques, il n'est pas approprié de rendre tout ce qui est ridiculement flexible, car le temps que vous passez à résumer des tâches ne sera pas un avantage.

Passer des interfaces à la place de classes concrètes signifie parfois, par exemple, que vous pouvez facilement permuter la lecture d’un fichier contre un flux réseau. Cependant, pour une grande quantité de projets logiciels, ce genre de flexibilité est tout simplement pas toujours va être nécessaire, et vous pourriez tout aussi bien passer des classes de fichiers en béton et l' appeler un jour et épargner vos cellules du cerveau.

Une partie de l’art du développement logiciel consiste à avoir une bonne idée de ce qui est susceptible de changer avec le temps et de ce qui ne l’est pas. Pour les éléments susceptibles de changer, utilisez les interfaces et autres concepts SOLID. Pour les choses qui ne le seront pas, utilisez YAGNI et transmettez simplement des types concrets, oubliez les classes d'usine, oubliez toute connexion et configuration d'exécution, etc., et oubliez beaucoup des abstractions SOLID. D'après mon expérience, l'approche YAGNI s'est révélée correcte bien plus souvent qu'elle ne l'est pas.

comment s'appelle-t-il
la source
19
Ma première introduction à SOLID concernait un nouveau système que nous construisions il y a environ 15 ans. Nous avons tous bu le kool aid man. Si quelqu'un a mentionné quelque chose qui ressemblait à YAGNI, nous étions comme "Pfffft ... plebeian". J'ai eu l'honneur (horreur?) De voir ce système évoluer au cours de la prochaine décennie. C'est devenu un gâchis difficile à comprendre que personne ne pouvait comprendre, pas même nous, les fondateurs. Les architectes aiment SOLID. Les personnes qui gagnent réellement leur vie aiment YAGNI. Ni est parfait, mais YAGNI est proche de la perfection, et devrait être votre défaut si vous ne savez pas ce que vous faites. :-)
Calphool le
11
@NWard Oui, nous l'avons fait sur un projet. Je suis allé avec des noix. Maintenant, nos tests sont impossibles à lire et à maintenir, en partie à cause de la sur-moquerie. De plus, en raison de l'injection de dépendance, il est très pénible de naviguer dans le code lorsque vous essayez de résoudre un problème. SOLID n'est pas une solution miracle. YAGNI n'est pas une solution miracle. Les tests automatisés ne sont pas une solution miracle. Rien ne peut vous éviter de penser à ce que vous faites et de décider si cela va aider ou entraver votre travail ou celui de quelqu'un d'autre.
jpmc26
22
Beaucoup de sentiment anti-SOLIDE ici. SOLID et YAGNI ne sont pas deux extrémités d'un spectre. Ils sont comme les coordonnées X et Y sur un graphique. Un bon système a très peu de code superflu (YAGNI) ET suit les principes SOLID.
Stephen
31
Meh, (a) je ne suis pas d'accord avec SOLID = enterprisey et (b) tout l'intérêt de SOLID est que nous avons tendance à être des préviseurs extrêmement pauvres de ce qui sera nécessaire. Je suis d'accord avec @Stephen ici. YAGNI dit qu'il ne faut pas essayer d'anticiper les exigences futures qui ne sont pas clairement énoncées aujourd'hui. SOLID dit que nous devons nous attendre à ce que la conception évolue avec le temps et à appliquer certaines techniques simples pour la faciliter. Ils ne sont pas mutuellement exclusifs. les deux sont des techniques d'adaptation à l'évolution des besoins. Les vrais problèmes surviennent lorsque vous essayez de concevoir pour des exigences peu claires ou très éloignées.
Aaronaught
8
"vous pouvez facilement échanger la lecture d'un fichier contre un flux réseau" - voici un bon exemple dans lequel une description trop simpliste de l'ID égare les gens. Les gens pensent parfois (en effet), "cette méthode utilisera une méthode File, alors elle prendra un IFiletravail effectué". Ensuite, ils ne peuvent pas facilement remplacer un flux réseau, car ils ont trop sollicité l'interface et que certaines IFileméthodes de la méthode ne l'utilisent même pas, qui ne s'appliquent pas aux sockets, de sorte qu'un socket ne peut pas être implémenté IFile. Une des choses pour lesquelles DI n’est pas une solution miracle, c’est inventer les bonnes abstractions (interfaces) :-)
Steve Jessop
11

En termes simples:

L'application du DIP est à la fois facile et amusante . Ne pas bien concevoir le modèle du premier coup n'est pas une raison suffisante pour abandonner le DIP.

  • En général, les IDE vous aident à effectuer ce type de refactoring. Certains vous permettent même d'extraire l'interface d'une classe déjà implémentée.
  • Il est presque impossible d'obtenir le bon design du premier coup
  • Le flux de travail normal implique de changer et de repenser les interfaces dans les premières étapes de développement.
  • À mesure que le développement évolue, il mûrit et vous aurez moins de raisons de modifier les interfaces
  • A un stade avancé, les interfaces (la conception) seront matures et ne changeront guère
  • À partir de ce moment, vous commencez à en récolter les fruits, car votre application est ouverte à l’extension.

D'autre part, la programmation avec des interfaces et OOD peut ramener la joie au métier parfois obsolète de la programmation.

Certaines personnes disent que cela ajoute de la complexité, mais je pense que l'opossite est vrai. Même pour les petits projets. Cela facilite les tests et les moqueries. Cela fait que votre code a moins d' caseinstructions, si elles sont imbriquées ifs. Il réduit la complexité cyclomatique et vous fait penser de manière nouvelle. Cela rend la programmation plus semblable à la conception et à la fabrication réelles.

Tulains Córdova
la source
5
Je ne sais pas quels langages ou IDE sont utilisés par l'OP, mais dans VS 2013, il est ridiculement simple de travailler avec des interfaces, d'extraire des interfaces et de les implémenter, et critique si vous utilisez TDD. Il n'y a pas de frais de développement supplémentaires pour développer en utilisant ces principes.
stephenbayer
Pourquoi cette réponse parle-t-elle de DI si la question portait sur DIP? DIP est un concept des années 1990, alors que DI est de 2004. Ils sont très différents.
Rogério
1
(Mon commentaire précédent était destiné à une autre réponse; ignorez-le.) "Programmation pour interfaces" est beaucoup plus général que DIP, mais il ne s'agit pas de faire en sorte que chaque classe implémente une interface distincte. Et cela ne facilite les "tests / moquages" que si les outils de tests / moquages ​​souffrent de limitations sérieuses.
Rogério
@ Rogério Habituellement, lors de l'utilisation de DI, toutes les classes n'implémentent pas une interface distincte. Une interface implémentée par plusieurs classes est commune.
Tulains Córdova
@ Rogério j'ai corrigé ma réponse, chaque fois que j'ai mentionné DI, je voulais dire DIP.
Tulains Córdova
9

Utilisez l' inversion de dépendance là où cela a du sens.

Un contre-exemple extrême est la classe "string" incluse dans de nombreuses langues. Il représente un concept primitif, essentiellement un tableau de caractères. En supposant que vous puissiez changer cette classe de base, cela n’a aucun sens d’utiliser D ici car vous n’aurez jamais besoin d’échanger l’état interne avec autre chose.

Si vous avez un groupe d'objets utilisés en interne dans un module qui ne sont exposés à aucun autre module ou réutilisés nulle part, l'utilisation de DI ne vaut probablement pas la peine.

À mon avis, il y a deux endroits où le DI devrait automatiquement être utilisé:

  1. Dans les modules conçus pour l'extension. Si l'objectif d'un module est de l'étendre et de changer de comportement, il est parfaitement logique d'intégrer l'ID dès le début.

  2. Dans les modules que vous refactoring dans le but de réutiliser le code. Vous avez peut-être codé une classe pour qu'elle fasse quelque chose, puis vous vous êtes rendu compte plus tard qu'avec un refactor, vous pouvez utiliser ce code ailleurs et il est nécessaire de le faire . C’est un excellent candidat pour DI et autres modifications d’extensibilité.

Les clés ici sont utilisées lorsque cela est nécessaire car cela introduira une complexité supplémentaire, et assurez-vous de mesurer ce besoin via des exigences techniques (point un) ou une révision de code quantitatif (point deux).

DI est un excellent outil, mais comme tout outil *, il peut être surutilisé ou mal utilisé.

* Exception à la règle ci-dessus: une scie alternative est l'outil idéal pour tout travail. Si cela ne résout pas votre problème, il sera supprimé. En permanence.


la source
3
Et si "ton problème" est un trou dans le mur? Une scie ne l'enlèverait pas; cela aggraverait les choses. ;)
Mason Wheeler
3
@MasonWheeler avec une scie puissante et amusante à utiliser, "un trou dans le mur" pourrait se transformer en "un portail", ce qui est un atout utile :-)
1
Tu ne peux pas utiliser une scie pour faire un patch pour le trou?
JeffO
Bien que le type non extensible par l'utilisateur présente certains avantages String, il existe de nombreux cas dans lesquels des représentations alternatives seraient utiles si le type comportait un bon ensemble d'opérations virtuelles (par exemple, copier une sous-chaîne dans une partie spécifiée d'un short[]rapport, indiquer si une la sous-chaîne contient ou peut contenir uniquement de l'ASCII, essayez de copier une sous-chaîne supposée ne contenir que de l'ASCII dans une partie spécifiée d'un byte[], etc.)
Supercat
1
Pourquoi cette réponse parle-t-elle de DI si la question portait sur DIP? DIP est un concept des années 1990, alors que DI est de 2004. Ils sont très différents.
Rogério
5

Il me semble que la question initiale manque une partie du point du DIP.

Je suis sceptique parce que nous payons un prix pour suivre ce principe. Supposons que j’ai besoin d’implémenter la fonctionnalité Z. Après analyse, je conclus que la fonctionnalité Z est composée des fonctionnalités A, B et C. Je crée une classe de fascades Z qui, par le biais d’interfaces, utilise les classes A, B et C. Je commence à coder les l’implémentation et, à un moment donné, je réalise que la tâche Z consiste en réalité en une fonctionnalité A, B et D. Maintenant, j’ai besoin de supprimer l’interface C, le prototype de classe C et d’écrire une interface et une classe D distinctes. Sans interfaces, seule la classe aurait besoin d'être remplacée.

Pour tirer pleinement parti du DIP, vous devez d’abord créer la classe Z et lui demander d’appeler les fonctionnalités des classes A, B et C (qui ne sont pas encore développées). Cela vous donne l'API pour les classes A, B et C. Ensuite, créez des classes A, B et C et renseignez les détails. Vous devriez effectivement créer les abstractions dont vous avez besoin lorsque vous créez la classe Z, en vous basant entièrement sur ce dont elle a besoin. Vous pouvez même écrire des tests autour de la classe Z avant même que les classes A, B ou C ne soient écrites.

Rappelez-vous que le DIP dit que "les modules de haut niveau ne devraient pas dépendre de modules de bas niveau. Les deux devraient dépendre d'abstractions".

Une fois que vous avez déterminé ce dont la classe Z a besoin et la manière dont elle veut obtenir ce dont elle a besoin, vous pouvez ensuite entrer les détails. Bien sûr, il faudra parfois apporter des modifications à la classe Z, mais 99% du temps, ce ne sera pas le cas.

Il n’y aura jamais de classe D parce que vous avez déterminé que Z a besoin de A, B et C avant d’être écrit. Un changement dans les exigences est une histoire totalement différente.

Stephen
la source
5

La réponse courte est "presque jamais", mais il y a en fait quelques endroits où le DIP n'a pas de sens:

  1. Usines ou constructeurs, dont le travail consiste à créer des objets. Ce sont essentiellement les "nœuds feuilles" dans un système qui englobe totalement l'IoC. À un moment donné, quelque chose doit réellement créer vos objets et ne peut dépendre de rien d'autre pour le faire. Dans de nombreuses langues, un conteneur IoC peut le faire pour vous, mais vous devez parfois le faire à l'ancienne.

  2. Implémentations de structures de données et d'algorithmes. En règle générale, dans ces cas, les caractéristiques essentielles pour lesquelles vous optimisez (telles que la durée d'exécution et la complexité asymptotique) dépendent des types de données spécifiques utilisés. Si vous implémentez une table de hachage, vous devez vraiment savoir que vous utilisez un tableau pour le stockage, pas une liste chaînée, et seule la table elle-même sait comment allouer correctement les tableaux. Vous ne voulez pas non plus passer dans un tableau mutable et demander à l'appelant de casser votre table de hachage en bidouillant son contenu.

  3. Classes de modèle de domaine . Ceux-ci implémentent votre logique métier et (la plupart du temps), il est logique de n'avoir qu'une seule implémentation, car (la plupart du temps), vous ne développez le logiciel que pour une seule entreprise. Bien que certaines classes de modèle de domaine puissent être construites à l'aide d'autres classes de modèle de domaine, cela se fera généralement au cas par cas. Etant donné que les objets de modèle de domaine n'incluent aucune fonctionnalité pouvant être utilement simulée, il n'y a aucun avantage de testabilité ou de maintenabilité pour le DIP.

  4. Tous les objets fournis en tant qu'API externe et devant créer d'autres objets dont vous ne souhaitez pas exposer publiquement les détails de la mise en œuvre. Cela relève de la catégorie générale "La conception de la bibliothèque est différente de la conception de l'application". Une bibliothèque ou un framework peut faire un usage libéral de DI en interne, mais devra éventuellement faire un travail réel, sinon ce n'est pas une bibliothèque très utile. Disons que vous développez une bibliothèque de réseau; vous ne voulez vraiment pas que le consommateur puisse fournir sa propre implémentation d'une prise. Vous pouvez utiliser en interne une abstraction d'un socket, mais l'API que vous exposez aux appelants va créer ses propres sockets.

  5. Tests unitaires et tests doubles. Les faux et les talons sont supposés faire une chose et le faire simplement. Si vous avez un faux assez complexe pour vous inquiéter de l'injection de dépendance, il est probablement trop complexe (peut-être parce qu'il implémente une interface beaucoup trop complexe).

Il pourrait y en avoir plus Ce sont ceux que je vois assez fréquemment.

Aaronaught
la source
Que diriez-vous de "chaque fois que vous travaillez dans un langage dynamique"?
Kevin
Non? Je fais beaucoup de travail en JavaScript et il s'applique toujours aussi bien là-bas. Le "O" et le "I" dans SOLID peuvent toutefois devenir un peu flous.
Aaronaught
Euh ... Je trouve que les types de première classe de Python, combinés à la frappe au canard, le rendent moins nécessaire.
Kevin
Le DIP n'a absolument rien à voir avec le système de types. Et de quelle manière les "types de première classe" sont-ils uniques à Python? Lorsque vous souhaitez tester quelque chose de manière isolée, vous devez remplacer les doublons de test par ses dépendances. Ces doubles de test peuvent être des implémentations alternatives d'une interface (dans des langages à typage statique) ou peuvent être des objets anonymes ou des types alternatifs ayant les mêmes méthodes / fonctions (typage de canard). Dans les deux cas, vous avez toujours besoin d'un moyen de remplacer une instance.
Aaronaught
1
@ Kevin Python n'était pas la première langue à posséder du typage dynamique ou des simulacres. C'est aussi complètement hors de propos. La question n'est pas de savoir quel type d'objet est, mais comment / où cet objet est créé. Lorsqu'un objet crée ses propres dépendances, vous êtes obligé de tester à l'unité ce qui devrait être des détails d'implémentation, en effectuant des choses horribles, telles que le remplacement des constructeurs de classes que l'API publique ne mentionne pas. Et oublier les tests, les comportements de mélange et la construction des objets conduit simplement à un couplage étroit. Taper du canard ne résout aucun de ces problèmes.
Aaronaught
2

Certains signes que vous appliquez DIP à un niveau trop bas, là où cela n’apporte aucune valeur:

  • vous avez un couple C / CImpl ou IC / C, avec une seule implémentation de cette interface
  • les signatures de votre interface et de votre implémentation correspondent individuellement (violation du principe DRY)
  • vous changez souvent C et CImpl en même temps.
  • C est interne à votre projet et n'est pas partagé en dehors de votre projet en tant que bibliothèque.
  • vous êtes frustré par F3 dans Eclipse / F12 dans Visual Studio qui vous amène à l'interface au lieu de la classe actuelle

Si c'est ce que vous voyez, il serait peut-être préférable que Z appelle C directement et ignore l'interface.

De même, je ne pense pas à la décoration de méthode par un cadre d'injection de dépendances / proxy dynamique (Spring, Java EE) de la même manière qu'un vrai SOLID DIP - cela ressemble plus à un détail d'implémentation du fonctionnement de la décoration de méthode dans cette pile technologique. La communauté Java EE considère qu’il s’agit d’une amélioration pour laquelle vous n’avez plus besoin des paires Foo / FooImpl comme vous le faisiez auparavant ( référence ). En revanche, Python prend en charge la décoration de fonction en tant que fonctionnalité de langage de premier ordre.

Voir aussi ce blog .

Wrschneider
la source
0

Si vous inversez toujours vos dépendances, alors toutes vos dépendances sont à l'envers. Ce qui signifie que si vous avez commencé avec du code en désordre avec un nœud de dépendances, c'est ce que vous avez encore (vraiment), simplement inversé. C'est là que se pose le problème suivant: chaque modification d'une implémentation doit également modifier son interface.

Le point d’inversion des dépendances est l’inversion sélective des dépendances qui rendent les choses enchevêtrées. Ceux qui devraient aller de A à B en C le sont toujours, ce sont ceux qui allaient de C à A qui passent maintenant de A à C.

Le résultat devrait être un graphique de dépendance exempt de cycles - un DAG. Il existe différents outils permettant de vérifier cette propriété et de tracer le graphique.

Pour une explication plus complète, voir cet article :

L’essence de l’application correcte du principe d’inversion de dépendance est la suivante:

Divisez le code / service /… vous dépendez dans une interface et une implémentation. L'interface restructure la dépendance dans le jargon du code en l'utilisant, l'implémentation l'implémente en termes de techniques sous-jacentes.

La mise en œuvre reste là où elle est. Mais l'interface a maintenant une fonction différente (et utilise un jargon / langage différent), décrivant quelque chose que le code utilisateur peut faire. Déplacez-le dans ce paquet. En ne plaçant pas l'interface et l'implémentation dans le même package, la dépendance (direction) est inversée utilisateur → implémentation en implémentation → utilisateur.

soru
la source