Quel est le but des conteneurs IOC? Les raisons combinées peuvent être simplifiées comme suit:
Lorsque vous utilisez les principes de développement OOP / SOLID, l’injection de dépendance est compliquée. Soit vous avez les points d’entrée de premier niveau qui gèrent les dépendances pour plusieurs niveaux en dessous d’eux-mêmes et les transmettent récursivement lors de la construction, ou vous avez un peu de code en double dans les modèles fabrique / constructeur et les interfaces qui construisent des dépendances selon vos besoins.
Il n’existe pas de solution OOP / SOLID pour effectuer cela ET d’ avoir un très beau code.
Si cette déclaration précédente est vraie, comment les conteneurs IOC le font-ils? Autant que je sache, ils n'utilisent pas une technique inconnue qui ne peut pas être réalisée avec une DI manuelle. La seule explication est que les conteneurs IOC enfreignent les principes de la POO / SOLID en utilisant des accesseurs privés d'objets statiques.
Les conteneurs IOC enfreignent-ils les principes suivants en coulisse? Telle est la véritable question, car je comprends bien, mais j'ai le sentiment que quelqu'un d'autre comprend mieux:
- Contrôle de la portée . La portée est la raison de presque toutes les décisions que je prends concernant la conception de mon code. Bloc, Local, Module, Statique / Global. La portée doit être très explicite, autant au niveau des blocs que de la statique autant que possible. Vous devriez voir les déclarations, les instanciations et les fins de cycle de vie. Je fais confiance au langage et au GC pour gérer la portée tant que je suis explicite avec elle. Dans le cadre de mes recherches, j'ai constaté que les conteneurs IOC configuraient la plupart ou toutes les dépendances en tant que statiques et les contrôlaient via une implémentation AOP en coulisse. Donc, rien n'est transparent.
- Encapsulation . Quel est le but de l'encapsulation? Pourquoi devrions-nous garder les membres privés ainsi? Pour des raisons pratiques, il est donc important que les développeurs de notre API ne puissent pas interrompre l'implémentation en modifiant l'état (qui devrait être géré par la classe de propriétaires). Mais aussi, pour des raisons de sécurité, il est impossible d'éviter les injections qui dépassent nos États membres et contournent la validation et le contrôle de classe. Donc, tout ce qui (les frameworks Mocking ou IOC) injecte du code avant la compilation pour permettre un accès externe à des membres privés est assez énorme.
- Principe de responsabilité unique . En surface, les conteneurs IOC semblent rendre les choses plus propres. Mais imaginez comment vous feriez la même chose sans les frameworks d'aide. Vous auriez des constructeurs avec une douzaine de dépendances passées. Cela ne veut pas dire que vous les recouvriez avec IOC Containers, c'est une bonne chose! C'est un signe de re-factoriser votre code et de suivre SRP.
Ouvert / Fermé . Tout comme SRP n’est pas réservé aux classes (j’applique SRP aux lignes à responsabilité unique, sans parler des méthodes). Open / Closed n'est pas simplement une théorie de haut niveau pour ne pas modifier le code d'une classe. C'est une pratique de comprendre la configuration de votre programme et de contrôler ce qui est modifié et ce qui est étendu. Les conteneurs IOC peuvent modifier partiellement le mode de fonctionnement de vos classes pour les raisons suivantes:
une. Le code principal ne détermine pas la commutation des dépendances, mais la configuration du framework.
b. La portée peut être modifiée à un moment qui n'est pas contrôlé par les membres appelants, elle est plutôt déterminée en externe par un cadre statique.
La configuration de la classe n'est donc pas vraiment fermée, elle se modifie elle-même en fonction de la configuration d'un outil tiers.
La raison pour laquelle c'est une question est parce que je ne suis pas nécessairement un maître de tous les conteneurs IOC. Et bien que l’idée d’un conteneur IOC soit intéressante, elle apparaît simplement comme une façade recouvrant une mauvaise mise en œuvre. Mais pour accomplir le contrôle externe de la portée, l’accès des membres privés et le chargement paresseux, il reste encore beaucoup à faire. La procédure AOP est excellente, mais la manière dont elle est réalisée via IOC Containers est également discutable.
Je peux faire confiance à C # et au .NET GC pour faire ce que j’attends. Mais je ne peux pas faire autant confiance à un outil tiers qui modifie mon code compilé pour effectuer des solutions de contournement à des tâches que je ne serais pas en mesure de faire manuellement.
EG: Entity Framework et d'autres ORM créent des objets fortement typés et les mappent vers des entités de base de données, tout en fournissant une fonctionnalité de base de la plaque de la chaudière pour l'exécution de CRUD. N'importe qui peut créer son propre ORM et continuer à suivre les principes de la POO / SOLID manuellement. Mais ces cadres nous aident à ne pas réinventer la roue à chaque fois. Alors que IOC Containers, semble-t-il, nous aide à contourner volontairement les principes OOP / SOLID et à les dissimuler.
IOC
etExplicitness of code
est exactement ce qui me pose problème. La DI manuelle peut facilement devenir une corvée, mais elle met au moins un flux de programme explicite autonome à un seul endroit: "Vous obtenez ce que vous voyez, vous voyez ce que vous obtenez". Alors que les reliures et les déclarations de conteneurs IOC peuvent facilement devenir un programme parallèle caché.Réponses:
Je vais passer en revue vos points numériquement, mais d’abord, il ya une chose à laquelle vous devriez faire très attention: ne confondez pas la façon dont un consommateur utilise une bibliothèque avec la façon dont la bibliothèque est mise en œuvre . Entity Framework (que vous citez vous-même comme une bonne bibliothèque) et MVC d'ASP.NET en sont de bons exemples. Les deux font beaucoup de choses sous le capot avec, par exemple, une réflexion, ce qui ne serait absolument pas considéré comme un bon design si vous le diffusiez dans le code quotidien.
Ces bibliothèques ne sont absolument pas "transparentes" dans leur fonctionnement ou ce qu'elles font en coulisse. Mais cela n’est pas préjudiciable, car ils appuient de bons principes de programmation chez leurs consommateurs . Ainsi, chaque fois que vous parlez d'une bibliothèque comme celle-ci, souvenez-vous qu'en tant que consommateur d'une bibliothèque, vous n'avez pas à vous soucier de son implémentation ou de sa maintenance. Vous ne devez vous soucier que de la manière dont cela aide ou gêne le code que vous écrivez et qui utilise la bibliothèque. Ne confondez pas ces concepts!
Donc, pour passer par point:
Nous arrivons immédiatement à ce que je peux seulement supposer être un exemple de ce qui précède. Vous dites que les conteneurs IOC configurent la plupart des dépendances de manière statique. Eh bien, peut-être que certains détails d’implémentation de leur fonctionnement incluent le stockage statique (bien que, étant donné qu’ils ont tendance à avoir une instance d’un objet comme Ninject
IKernel
comme stockage principal, je doute même de cela). Mais ce n'est pas votre préoccupation!En fait, un conteneur IoC est tout aussi explicite et complet que l'injection de dépendance du pauvre. (Je vais continuer à comparer les conteneurs IoC aux DI du pauvre, car les comparer à aucune DI serait injuste et déroutant. Et, pour être clair, je n'utilise pas "le DI du pauvre" comme péjoratif.)
Dans le DI du pauvre, vous instanciez une dépendance manuellement, puis l'injectez dans la classe qui en a besoin. Au moment où vous construisez la dépendance, vous choisissez quoi faire: stockez-la dans une variable localisée, une variable de classe, une variable statique, ne la stockez pas du tout, peu importe. Vous pouvez transmettre la même instance à un grand nombre de classes ou en créer une nouvelle pour chaque classe. Peu importe. Le fait est que, pour voir ce qui se passe, vous devez rechercher le point, probablement près de la racine de l'application, où la dépendance est créée.
Maintenant, qu'en est-il un conteneur IoC? Eh bien, vous faites exactement la même chose! Encore une fois, en passant par la terminologie Ninject, vous regardez où la liaison est mis en place, et de trouver si elle dit quelque chose comme
InTransientScope
,InSingletonScope
ou autre chose. Si quelque chose est potentiellement plus clair, parce que vous avez du code déclarant sa portée, plutôt que de devoir regarder à travers une méthode pour suivre ce qui arrive à un objet (un objet peut être limité à un bloc, mais est-il utilisé plusieurs fois dans cette bloquer ou juste une fois, par exemple). Alors peut-être que l'idée de devoir définir une portée sur le conteneur IoC plutôt que sur une langue brute vous rebute, mais que vous ayez confiance en votre bibliothèque IoC - ce que vous devriez faire! - cela ne présente pas de réel inconvénient. .Je ne sais toujours pas vraiment de quoi vous parlez ici. Les conteneurs IoC considèrent-ils les propriétés privées dans le cadre de leur mise en œuvre interne? Je ne sais pas pourquoi ils le feraient, mais encore une fois, s'ils le font, la façon dont une bibliothèque que vous utilisez est mise en œuvre ne vous concerne pas.
Ou peut-être exposent-ils une capacité telle que l'injection dans des setters privés? Honnêtement, je n'ai jamais rencontré cela, et je doute que ce soit une caractéristique commune. Mais même s’il existe, c’est un simple cas d’outil qui peut être mal utilisé. N'oubliez pas que même sans conteneur IoC, ce ne sont que quelques lignes de code Reflection permettant d'accéder à une propriété privée et de la modifier. C'est quelque chose que vous ne devriez presque jamais faire, mais cela ne veut pas dire que .NET est mauvais pour exposer cette capacité. Si quelqu'un abuse si manifestement et si sauvagement d'un outil, c'est sa faute, pas son outil.
Le point ultime ici est similaire à 2. La plupart du temps, les conteneurs IoC utilisent le constructeur! L'injection de poseuse est proposée dans des cas très spécifiques où, pour des raisons particulières, l'injection de constructeur ne peut pas être utilisée. Toute personne qui utilise l'injection de setter tout le temps pour masquer le nombre de dépendances transmises est totalement absurde de l'outil. Ce n'est PAS la faute de l'outil, c'est la leur.
Maintenant, si c’était une erreur très facile à faire innocemment, et que les conteneurs IoC encouragent, alors d’accord, peut-être auriez-vous raison. Ce serait comme rendre chaque membre de ma classe public, puis blâmer les autres quand ils modifient des choses qu'ils ne devraient pas, non? Mais toute personne qui utilise l'injection de setter pour couvrir des violations du SRP ignore soit totalement les principes de conception de base, soit en ignore totalement. Il est déraisonnable d'en imputer la responsabilité au conteneur IoC.
C'est particulièrement vrai parce que c'est aussi quelque chose que vous pouvez faire aussi facilement avec la DI du pauvre homme:
Donc, vraiment, cette inquiétude semble totalement orthogonale à l'utilisation ou non d'un conteneur IoC.
Changer la façon dont vos classes fonctionnent ensemble n'est pas une violation d'OCP. Si c'était le cas, alors toute inversion de dépendance encouragerait une violation de l'OCP. Si c'était le cas, ils ne seraient pas tous les deux dans le même acronyme SOLID!
En outre, ni les points a) ni b) n’ont rien à voir avec OCP. Je ne sais même pas comment répondre à ces questions en ce qui concerne OCP.
La seule chose que je peux deviner, c’est que, selon vous, OCP est lié au fait que le comportement n’est pas modifié au moment de l’exécution ou à la source de contrôle du cycle de vie des dépendances dans le code. Ce n'est pas. OCP consiste à ne pas avoir à modifier le code existant lorsque des exigences sont ajoutées ou modifiées. Il s'agit d' écrire du code, pas de la manière dont le code que vous avez déjà écrit est collé (bien que, bien sûr, un couplage lâche soit un élément important de la réalisation de l'OCP).
Et une dernière chose que vous dites:
Oui, vous pouvez . Vous n'avez absolument aucune raison de penser que ces outils, sur lesquels s'appuient un grand nombre de projets, sont plus bogués ou sujets à un comportement inattendu que toute autre bibliothèque tierce.
Addenda
Je viens de remarquer que vos paragraphes d'introduction pourraient également utiliser un adressage. Vous dites avec ironie que les conteneurs IoC "n'utilisent pas une technique secrète dont nous n'avons jamais entendu parler" pour éviter un code compliqué, sujet à la duplication, afin de créer un graphe de dépendance. Et vous avez tout à fait raison, ce qu’ils font, c’est vraiment résoudre ces problèmes avec les mêmes techniques de base que les programmeurs.
Permettez-moi de vous parler d'un scénario hypothétique. En tant que programmeur, vous créez une application volumineuse et, au point d’entrée, où vous construisez votre graphe d’objets, vous remarquez que le code est assez confus. Il y a pas mal de classes qui sont utilisées encore et encore, et chaque fois que vous en construisez une, vous devez reconstruire toute la chaîne de dépendances. De plus, vous ne disposez d'aucun moyen expressif de déclarer ou de contrôler le cycle de vie des dépendances, à l'exception d'un code personnalisé pour chacune d'entre elles. Votre code est non structuré et plein de répétition. C'est le désordre dont vous parlez dans votre paragraphe d'introduction.
Commençons donc par un peu de refactorisation, dans laquelle un code répété est suffisamment structuré pour être intégré dans des méthodes d'assistance, etc. Mais alors vous commencez à penser: est-ce un problème que je pourrais peut-être aborder de manière générale , un problème qui n'est pas spécifique à ce projet particulier mais qui pourrait vous aider dans tous vos projets futurs?
Alors, assoyez-vous et réfléchissez-y, et décidez qu'il devrait y avoir une classe capable de résoudre les dépendances. Et vous décrivez les méthodes publiques dont il aurait besoin:
Bind
dit "où vous voyez un argument constructeur de typeinterfaceType
, passez une instance deconcreteType
". Lesingleton
paramètre supplémentaire indique s'il faut utiliser la même instance àconcreteType
chaque fois ou en créer une nouvelle.Resolve
essayera simplement de construireT
avec n'importe quel constructeur, il peut trouver dont les arguments sont tous des types qui ont été liés auparavant. Il peut également s'appeler de manière récursive pour résoudre les dépendances jusqu'au bout. S'il ne peut pas résoudre une instance parce que tout n'a pas été lié, il lève une exception.Vous pouvez essayer de l'implémenter vous-même, et vous constaterez que vous avez besoin d'un peu de réflexion et d'une mise en cache pour les liaisons là où
singleton
c'est vrai, mais certainement rien de radical ou d'horrifiant. Et une fois que vous avez terminé, le tour est joué , vous avez le cœur de votre propre conteneur IoC! Est-ce vraiment si effrayant? La seule différence réelle entre ceci et Ninject ou StructureMap ou Castle Windsor ou celui que vous préférez est que ceux-ci ont beaucoup plus de fonctionnalités pour couvrir les (nombreux!) Cas d'utilisation où cette version de base ne serait pas suffisante. Mais au fond, ce que vous avez là est l’essence d’un conteneur IoC.la source
En théorie? Les conteneurs IoC ne sont qu'un outil. Vous pouvez avoir des objets qui ont une responsabilité unique, des interfaces bien séparées, leur comportement ne peut pas être modifié une fois écrits, etc.
En pratique? Absolument.
J'ai entendu dire que peu de programmeurs ont déclaré qu'ils utilisaient des conteneurs IoC spécifiquement pour vous permettre d'utiliser une configuration pour modifier le comportement du système, en violation du principe d'ouverture / fermeture. Il y avait des blagues sur le codage en XML car 90% de leur travail consistait à corriger la configuration de Spring. Et vous avez certainement raison de dire que masquer les dépendances (ce qui n’est pas le cas de tous les conteneurs IoC) est un moyen rapide et facile de créer un code hautement couplé et très fragile.
Regardons les choses différemment. Considérons une classe très basique ayant une seule dépendance basique. Cela dépend d'un
Action
, d'un délégué ou d'un foncteur qui ne prend aucun paramètre et retournevoid
. Quelle est la responsabilité de cette classe? Vous ne pouvez pas dire. Je pourrais nommer cette dépendance quelque chose de clair,CleanUpAction
qui vous fait penser que cela fait ce que vous voulez. Dès que l'IoC entre en jeu, il devient n'importe quoi . N'importe qui peut entrer dans la configuration et modifier votre logiciel pour faire des choses assez arbitraires."En quoi est-ce différent de passer au
Action
constructeur?" tu demandes. C'est différent parce que vous ne pouvez pas changer ce qui est transmis au constructeur une fois le logiciel déployé (ou au moment de l'exécution!). C'est différent parce que vos tests unitaires (ou vos tests unitaires de consommateurs) ont couvert les scénarios dans lesquels le délégué est passé au constructeur."Mais c'est un exemple bidon! Mes affaires ne permettent pas de changer de comportement!" t'exclamons. Désolé de vous dire, mais c'est le cas. À moins que l'interface que vous injectez ne soit que des données. Et votre interface ne se limite pas à des données, car s'il ne s'agissait que de données, vous construiriez un objet simple et l'utiliseriez. Dès que vous avez une méthode virtuelle dans votre point d’injection, le contrat de votre classe utilisant IoC est "Je peux tout faire ".
"En quoi cela diffère-t-il de l'utilisation de scripts pour piloter des choses? C'est parfaitement correct." Certains d'entre vous demandent. Ce n'est pas différent Du point de vue de la conception du programme, il n'y a pas de différence. Vous appelez votre programme pour exécuter du code arbitraire. Et bien sûr, cela se fait dans les jeux depuis des lustres. Cela se fait dans des tonnes d'outils d'administration de systèmes. Est-ce la POO? Pas vraiment.
Il n'y a aucune excuse pour leur omniprésence actuelle. Les conteneurs IoC ont un seul objectif: coller vos dépendances lorsque vous avez autant de combinaisons ou si elles se déplacent si rapidement qu'il est impossible de rendre ces dépendances explicites. Donc, très peu d’entreprises correspondent réellement à ce scénario.
la source
L'utilisation d'un conteneur IoC enfreint-elle nécessairement les principes de la POO / SOLID? Non .
Pouvez-vous utiliser les conteneurs IoC de manière à enfreindre les principes de la POO / SOLID? Oui .
Les conteneurs IoC ne sont que des cadres de gestion des dépendances dans votre application.
Vous avez mentionné les injections paresseuses, les fixateurs de propriétés et quelques autres fonctionnalités que je n’ai pas encore ressenties le besoin d’utiliser, qui, à mon avis, peuvent vous donner une conception moins qu'optimale. Cependant, les utilisations de ces fonctionnalités spécifiques sont dues aux limitations de la technologie dans laquelle vous implémentez des conteneurs IoC.
Par exemple, ASP.NET WebForms, vous devez utiliser l'injection de propriété, car il n'est pas possible d'instancier un
Page
. Cela est compréhensible car WebForms n’a jamais été conçu dans l’esprit des paradigmes de POO stricts.ASP.NET MVC, d’autre part, il est tout à fait possible d’utiliser un conteneur IoC en utilisant l’injection de constructeur très favorable OOP / SOLID.
Dans ce scénario (utilisant l'injection de constructeur), je crois qu'il promeut les principes SOLID pour les raisons suivantes:
Principe de responsabilité unique - Le fait d'avoir de nombreuses classes plus petites permet à ces classes d'être très spécifiques et d'avoir une raison d'être. L'utilisation d'un conteneur IoC facilite grandement leur organisation.
Open / Closed - C'est là où l'utilisation d'un conteneur IoC est vraiment brillante. Vous pouvez étendre les fonctionnalités d'une classe en injectant différentes dépendances. Avoir des dépendances que vous injectez vous permet de modifier le comportement de cette classe. Un bon exemple consiste à modifier la classe que vous injectez en écrivant à un décorateur pour lui ajouter une mise en cache ou une journalisation. Vous pouvez complètement changer les implémentations de vos dépendances si elles sont écrites dans un niveau d'abstraction approprié.
Principe de substitution de Liskov - Pas vraiment pertinent
Principe de séparation des interfaces - Vous pouvez affecter plusieurs interfaces à une seule heure concrète si vous le souhaitez. Tout conteneur IoC valant un grain de sel le fera facilement.
Inversion de dépendance - Les conteneurs IoC vous permettent d'effectuer facilement l'inversion de dépendance.
À la fois explicite et transparente, vous devez indiquer à votre conteneur IoC comment connecter des dépendances et gérer les durées de vie des objets. Cela peut être par convention, par fichiers de configuration ou par code écrit dans les applications principales de votre système.
C’est toute la raison pour OOP / SOLID, de rendre les systèmes gérables. L'utilisation d'un conteneur IoC est en ligne avec cet objectif.
Une classe avec 12 dépendances est probablement une violation SRP quel que soit le contexte dans lequel vous l'utilisez.
la source
Le
static
mot-clé en C # permet aux développeurs de casser beaucoup de principes de la POO / SOLID, mais il reste un outil valable dans les bonnes circonstances.Les conteneurs IoC permettent un code bien structuré et réduisent le fardeau cognitif des développeurs. Un conteneur IoC peut être utilisé pour faciliter le respect des principes SOLID. Bien sûr, il peut être utilisé à mauvais escient, mais si nous excluions l’utilisation de tout outil susceptible d’être utilisé à mauvais escient, nous devions abandonner complètement la programmation.
Ainsi, bien que ce discours soulève des arguments valables à propos d'un code mal écrit, ce n'est pas exactement une question et ce n'est pas exactement utile.
la source
Je vois plusieurs erreurs et malentendus dans votre question. La première est qu’il manque la distinction entre l’injection de propriété / de champ, l’injection de constructeur et le localisateur de services. Il y a eu beaucoup de discussions (p. Ex. Guerres à la flamme) sur la meilleure façon de faire les choses. Dans votre question, vous semblez ne présumer que de l'injection de propriété et ignorer l'injection du constructeur.
Deuxièmement, vous supposez que les conteneurs IoC affectent en quelque sorte la conception de votre code. Ils ne le font pas, ou du moins, il ne devrait pas être nécessaire de changer la conception de votre code si vous utilisez IoC. Vos problèmes avec les classes avec de nombreux constructeurs sont le cas où IoC cache de vrais problèmes avec votre code (le plus souvent une classe qui a des ruptures de SRP) et les dépendances cachées sont l’une des raisons pour lesquelles les gens découragent l’utilisation de l’injection de propriété et du localisateur de services.
Je ne comprends pas où réside le problème du principe ouvert-fermé, je ne ferai donc aucun commentaire à ce sujet. Mais il vous manque une partie de SOLID, qui a une influence majeure sur ceci: principe d'inversion de dépendance. Si vous suivez DIP, vous découvrirez que l'inversion d'une dépendance introduit une abstraction dont l'implémentation doit être résolue lors de la création d'une instance de classe. Avec cette approche, vous obtiendrez un grand nombre de classes, qui nécessitent la réalisation de plusieurs interfaces et sont fournies à la construction pour fonctionner correctement. Si vous deviez alors fournir ces dépendances à la main, vous auriez à faire beaucoup de travail supplémentaire. Les conteneurs IoC facilitent ce travail et vous permettent même de le changer sans avoir à recompiler le programme.
Donc, la réponse à votre question est non. Les conteneurs IoC ne violent pas les principes de conception de la POO. Bien au contraire, ils résolvent les problèmes causés par le respect des principes SOLID (notamment le principe de dépendance-inversion).
la source