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:
var myObject
= new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
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:
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.
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:
void Bind(Type interfaceType, Type concreteType, bool singleton);
T Resolve<T>();
Bind
dit "où vous voyez un argument constructeur de type interfaceType
, passez une instance de concreteType
". Le singleton
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 construire T
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.
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é.