J'essaie actuellement de comprendre SOLID. Ainsi, le principe d'inversion de dépendance signifie que deux classes doivent communiquer via des interfaces, pas directement. Exemple: si class A
possède 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 également à ouvrir/fermer.
À condition d'avoir bien compris, ma question serait est-ce une bonne pratique de l'appliquer à toutes les interactions de classe ou devrais-je essayer de penser en termes de couches ?
La raison pour laquelle je suis sceptique est que nous payons un certain prix pour suivre ce principe. Dis, j'ai besoin d'implémenter la fonction Z
. Après analyse, je conclus que la fonction Z
se compose des fonctionnalités A
, B
et C
. Je crée une façade classe Z
, qui, via les interfaces, utilise les classes A
, B
et C
. Je commence à coder l'implémentation et à un moment donné, je me rends compte que la tâche Z
consiste en fait en fonctionnalités A
, B
et D
. Maintenant, je dois supprimer l'interface C
, le prototype de classe C
et écrire une interface et une classe D
distinctes. 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 une implémentation directement couplée python, je ne devrais changer que l'implémentation.
Dans de nombreux dessins animés ou 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 histoire ici, au lieu du bien et du mal, nous avons SOLID sur une épaule, et YAGNI (vous n'en aurez pas besoin!) Assis sur l'autre.
Les principes SOLIDES poussés au maximum sont les mieux adaptés aux systèmes d'entreprise énormes, complexes et ultra configurables. Pour des systèmes plus petits ou plus spécifiques, il n'est pas approprié de rendre tout ridiculement flexible, car le temps que vous passez à résumer les choses ne s'avérera pas être un avantage.
Passer des interfaces au lieu de classes concrètes signifie parfois, par exemple, que vous pouvez facilement échanger la lecture d'un fichier pour un flux réseau. Cependant, pour une grande quantité de projets logiciels, ce type de flexibilité ne sera tout simplement pas jamais nécessaire, et vous pourriez tout aussi bien passer un fichier concret cours et appelez-le un jour et épargnez vos cellules cérébrales.
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 change pas. Pour les choses susceptibles de changer, utilisez les interfaces et autres SOLID concepts. Pour les choses qui ne changeront pas, utilisez YAGNI et passez simplement des types concrets, oubliez les classes d'usine, oubliez tous les connexion et configuration du runtime, etc., et oubliez beaucoup des abstractions SOLID. D'après mon expérience, l'approche YAGNI s'est avérée correcte plus souvent qu'elle ne l'est.
Dans les mots du profane:
L'application du DIP est à la fois simple et amusante. Ne pas obtenir le bon design à la première tentative n'est pas une raison suffisante pour abandonner complètement le DIP.
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 vraie. Même pour les petits projets. Cela facilite les tests/moqueries. Cela rend votre code avoir moins d'instructions case
ou imbriquées ifs
. Il réduit la complexité cyclomatique et vous fait penser différemment. Il rend la programmation plus semblable à la conception et à la fabrication du monde réel.
Utilisez 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 DI 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 pas exposés à d'autres modules ou réutilisés n'importe où, cela ne vaut probablement pas la peine d'utiliser DI.
Il y a deux endroits où DI devrait être automatiquement utilisé à mon avis:
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 de créer DI dès le début.
Dans les modules que vous refactorisez dans le but de réutiliser le code. Peut-être que vous avez codé une classe pour faire quelque chose, puis réalisez plus tard qu'avec un refactoriseur vous pouvez utiliser ce code ailleurs et il y a un besoin de le faire . C'est un excellent candidat pour DI et d'autres changements d'extensibilité.
Les clés ici sont l'utiliser là où c'est nécessaire car cela introduira une complexité supplémentaire, et assurez-vous de mesurer besoin soit par le biais d'exigences techniques (point un) ou d'une révision quantitative du code (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 parfait pour n'importe quel travail. S'il ne résout pas votre problème, il le supprimera. En permanence.
Il me semble que la question d'origine manque une partie du point du DIP.
La raison pour laquelle je suis sceptique est que nous payons un certain prix pour suivre ce principe. Disons, je dois implémenter la fonctionnalité Z. Après analyse, je conclus que la fonctionnalité Z se compose des fonctionnalités A, B et C. Je crée une classe de fascade Z, qui, via les interfaces, utilise les classes A, B et C.Je commence à coder le implémentation et à un moment donné, je me rends compte que la tâche Z se compose en fait des fonctionnalités A, B et D. Maintenant, je dois supprimer l'interface C, le prototype de classe C et écrire une interface et une classe D distinctes. Sans interfaces, seule la classe aurait besoin d'être remplacée.
Pour vraiment profiter du DIP, vous devez d'abord créer la classe Z et lui faire appeler la fonctionnalité 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, vous allez créer des classes A, B et C et remplir les détails. Vous devez effectivement créer les abstractions dont vous avez besoin pendant que vous créez la classe Z, en fonction entièrement de ce dont la classe Z 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.
N'oubliez pas que le DIP dit que "les modules de haut niveau ne doivent pas dépendre de modules de bas niveau. Les deux doivent dépendre d'abstractions".
Une fois que vous avez déterminé ce dont la classe Z a besoin et la façon dont elle veut obtenir ce dont elle a besoin, vous pouvez alors remplir les détails. Bien sûr, des modifications devront parfois être apportées à 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 compris que Z a besoin de A, B et C avant d'être écrit. Un changement dans les exigences est une toute autre histoire.
La réponse courte est "presque jamais", mais il y a, en fait, quelques endroits où le DIP n'a pas de sens:
Usines ou constructeurs, dont le travail consiste à créer des objets. Ce sont essentiellement les "nœuds feuilles" dans un système qui embrasse pleinement 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 parfois vous devez le faire à l'ancienne.
Implémentations de structures de données et d'algorithmes. En règle générale, dans ces cas, les principales caractéristiques que vous optimisez (telles que le temps 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 travaillez avec un tableau pour le stockage, pas une liste lié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 tripotant son contenu.
modèle de domaine classes. Ceux-ci implémentent votre logique métier et (la plupart du temps) cela n'a de sens que d'avoir une implémentation, car (la plupart du temps) vous ne développez le logiciel que pour une entreprise. Bien que certaines classes de modèle de domaine puissent être construites en utilisant d'autres classes de modèle de domaine, cela se fera généralement au cas par cas. Étant donné que les objets de modèle de domaine n'incluent aucune fonctionnalité pouvant être utilement moquée, le DIP ne présente aucun avantage de testabilité ou de maintenabilité.
Tous les objets fournis en tant qu'API externe et qui doivent créer d'autres objets dont vous ne souhaitez pas exposer les détails d'implémentation publiquement. Cela relève de la catégorie générale de "la conception de bibliothèque est différente de la conception d'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éseautage; vous ne voulez vraiment pas que le consommateur puisse fournir sa propre implémentation d'une socket. Vous pouvez utiliser en interne l'abstraction d'un socket, mais l'API que vous exposez aux appelants va créer ses propres sockets.
Tests unitaires et tests doubles. Les contrefaçons et les talons sont censés faire une chose et le faire simplement. Si vous avez un faux assez complexe pour vous inquiéter de faire ou non une injection de dépendance, alors c'est probablement trop complexe (peut-être parce qu'il implémente une interface qui est aussi beaucoup trop complexe).
Il pourrait y en avoir plus; ce sont ceux que je vois sur quelque peu sur une base fréquente.
Certains signes que vous appliquez DIP à un niveau trop micro, où il n'apporte pas de valeur:
Si c'est ce que vous voyez, vous feriez mieux de simplement appeler Z directement C et de sauter l'interface.
De plus, 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 que true 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 cela comme une amélioration que vous n'avez pas besoin de paires Foo/FooImpl comme vous le faisiez auparavant ( référence ). En revanche, Python prend en charge la décoration de fonction comme une fonctionnalité de langage de première classe .
Voir aussi cet article de blog .
Si vous inversez toujours vos dépendances, toutes vos dépendances sont à l'envers. Ce qui signifie que si vous avez commencé avec du code désordonné avec un nœud de dépendances, c'est ce que vous avez encore (vraiment), juste inversé. C'est là que vous obtenez le problème que chaque modification d'une implémentation doit également changer son interface.
Le point d'inversion de dépendance est que vous sélectivement inversez les dépendances qui font que les choses s'emmêlent. Ceux qui devaient passer de A à B à C le font toujours, ce sont ceux qui allaient de C à A qui vont maintenant de A à C.
Le résultat devrait être un graphe de dépendances exempt de cycles - un DAG. Il y a diversoutils qui vérifiera cette propriété et dessinera le graphique.
Pour une explication plus complète, voir cet article :
L'essence d'appliquer correctement le principe d'inversion de dépendance est la suivante:
Divisez le code/service /… dont vous dépendez en une interface et une implémentation. L'interface restructure la dépendance dans le jargon du code qui l'utilise, l'implémentation l'implémente en fonction de ses 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 utilisant peut faire. Déplacez-le dans ce package. En ne plaçant pas l'interface et l'implémentation dans le même package, la (direction de) la dépendance est inversée de l'utilisateur → implémentation à implémentation → utilisateur.