web-dev-qa-db-fra.com

IOC Conteneurs cassés OOP Principes

Quel est le but de IOC Containers? 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 devient désordonnée. Soit vous disposez des points d'entrée de niveau supérieur gérant les dépendances pour plusieurs niveaux en dessous d'eux-mêmes et passant les dépendances de manière récursive à travers la construction, soit vous avez un peu de code en double dans les modèles d'usine/constructeur et les interfaces qui créent des dépendances selon vos besoins.

Il n'y a aucun moyen OOP/SOLID pour effectuer cela [~ # ~] et [~ # ~] ont un code super joli .

Si cette déclaration précédente est vraie, alors comment faire IOC Conteneurs? Pour autant que je sache, ils n'utilisent pas une technique inconnue qui ne peut pas être faite avec DI manuelle Donc, le seul l'explication est que IOC Les conteneurs cassent les principes OOP/SOLID en utilisant des accesseurs privés d'objets statiques.

Est-ce que IOC Les conteneurs cassent les principes suivants dans les coulisses? C'est la vraie question car j'ai une bonne compréhension, mais j'ai le sentiment que quelqu'un d'autre a une meilleure compréhension:

  1. Contrôle de la portée. La portée est la raison de presque toutes les décisions que je prends sur la conception de mon code. Block, Local, Module, Static/Global. La portée doit être très explicite, autant au niveau du bloc et aussi peu que possible au niveau statique. Vous devriez voir des déclarations, des instanciations et des 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 lui. Dans mes recherches, j'ai découvert que IOC Les conteneurs configurent la plupart ou toutes les dépendances en tant que statiques et les contrôlent via une implémentation AOP en arrière-plan. Donc, rien n'est transparent.
  2. Encapsulation. Quel est le but de l'encapsulation? Pourquoi devrions-nous garder les membres privés ainsi? Pour des raisons pratiques, les implémenteurs de notre API ne peuvent pas interrompre l'implémentation en modifiant l'état (qui doit être géré par la classe propriétaire). Mais aussi, pour des raisons de sécurité, il est impossible que des injections dépassent nos États membres et contournent la validation et le contrôle de classe. Donc tout ce qui (frameworks Mocking ou IOC) qui injecte du code avant le temps de compilation pour permettre l'accès externe aux membres privés est assez énorme.
  3. principe de responsabilité unique. En surface, IOC Les conteneurs semblent rendre les choses plus propres. Mais imaginez comment vous pourriez accomplir la même chose sans les cadres d'assistance. Vous auriez des constructeurs avec une douzaine de dépendances transmises. ne signifie pas le couvrir avec IOC Containers, c'est une bonne chose! C'est un signe pour re-factoriser votre code et suivre SRP.
  4. Ouvert/Fermé. Tout comme SRP n'est pas de classe uniquement (j'applique SRP aux lignes à responsabilité unique, sans parler des méthodes). Open/Closed n'est pas seulement une théorie de haut niveau pour ne pas altérer le code d'une classe. Il s'agit de comprendre la configuration de votre programme et de contrôler ce qui est modifié et ce qui est étendu. IOC Les conteneurs peuvent modifier partiellement le fonctionnement de vos classes car:

    • une. Le code principal ne fait pas la détermination de la désactivation des dépendances, la configuration du framework l'est.

    • b. La portée peut être modifiée à un moment qui n'est pas contrôlé par les membres appelants, mais déterminé à l'extérieur par un cadre statique.

La configuration de la classe n'est donc pas vraiment fermée, elle se modifie en fonction de la configuration d'un outil tiers.

La raison pour laquelle il s'agit d'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 est Nice, ils apparaissent) pour être juste une façade qui cache une mauvaise mise en œuvre. Mais pour réaliser le contrôle externe de la portée, l'accès des membres privés et le chargement paresseux, beaucoup de choses douteuses non triviales doivent continuer. L'AOP est génial, mais la façon dont il est accompli via IOC Containers est également discutable.

Je peux faire confiance à C # et au GC .NET pour faire ce que j'attends. Mais je ne peux pas faire confiance à un outil tiers qui modifie mon code compilé pour effectuer des solutions de contournement à des choses que je ne pourrais pas faire manuellement.

E.G .: Entity Framework et d'autres ORM créent des objets fortement typés et les mappent à des entités de base de données, ainsi que fournissent des fonctionnalités de base de plaque chauffante pour effectuer CRUD. N'importe qui pouvait créer son propre ORM et continuer à suivre manuellement les principes OOP/SOLID. Mais ces cadres nous aident à ne pas avoir à réinventer la roue à chaque fois. Alors que IOC Les conteneurs, semble-t-il, nous aident à contourner délibérément les principes OOP/SOLID et à les couvrir.

51
Suamere

Je vais passer en revue vos points numériquement, mais d'abord, il y a quelque chose que vous devez 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 implémentée . De bons exemples de cela sont Entity Framework (que vous citez vous-même comme une bonne bibliothèque) et MVC d'ASP.NET. Les deux font énormément sous le capot avec, par exemple, la réflexion, ce qui ne serait absolument pas considéré comme un bon design si vous le diffusez au travers du code au jour le jour.

Ces bibliothèques sont absolument pas "transparentes" dans leur fonctionnement ou ce qu'elles font en coulisses. Mais ce n'est pas un inconvénient, car ils prennent en charge de bons principes de programmation dans leurs consommateurs . Donc, chaque fois que vous parlez d'une bibliothèque comme celle-ci, n'oubliez pas qu'en tant que consommateur d'une bibliothèque, ce n'est pas à vous de vous soucier de sa mise en œuvre ou de sa maintenance. Vous ne devez vous soucier que de la façon dont cela aide ou entrave le code que vous écrivez qui utilise la bibliothèque. Ne confondez pas ces concepts!

Donc, pour passer par point:

  1. Immédiatement, nous arrivons à ce que je ne peux que supposer est un exemple de ce qui précède. Vous dites que IOC définissent la plupart des dépendances comme statiques. Eh bien, peut-être que certains détails d'implémentation de leur fonctionnement incluent le stockage statique (bien qu'étant donné qu'ils ont tendance à avoir une instance d'un objet comme le _ de Ninject IKernel comme stockage de base, j'en doute même.) Mais ce n'est pas votre problème!

    En fait, un conteneur IoC est tout aussi explicite avec une portée que l'injection de dépendance d'un pauvre. (Je vais continuer à comparer les conteneurs IoC à l'ID du pauvre parce que les comparer à aucune ID ne serait injuste et déroutant. Et, pour être clair, je n'utilise pas "l'ID du pauvre" comme péjoratif.)

    Dans l'ID 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 choisiriez ce que vous en feriez - la stocker dans une variable de portée locale, une variable de classe, une variable statique, ne la stocke pas du tout, peu importe. Vous pouvez passer la même instance dans de nombreuses classes, ou vous pouvez en créer une nouvelle pour chaque classe. Peu importe. Le point est, pour voir ce qui se passe, vous regardez au point - probablement près de la racine de l'application - où la dépendance est créée.

    Qu'en est-il maintenant d'un conteneur IoC? Eh bien, vous faites exactement la même chose! Encore une fois, en utilisant la terminologie Ninject, vous regardez où la liaison est configurée et trouvez si elle dit quelque chose comme InTransientScope, InSingletonScope, ou autre chose. Si quelque chose est potentiellement plus clair, car vous avez du code déclarant sa portée, plutôt que d'avoir à parcourir une méthode pour suivre ce qui arrive à un objet (un objet peut être délimité par un bloc, mais est-il utilisé plusieurs fois dans ce bloc ou une seule fois, par exemple). Alors peut-être que vous êtes repoussé par l'idée de devoir utiliser une fonctionnalité sur le conteneur IoC plutôt qu'une fonctionnalité de langage brut pour dicter la portée, mais tant que vous faites confiance à votre bibliothèque IoC - ce que vous devriez! - il n'y a pas de réel inconvénient .

  2. Je ne sais toujours pas vraiment de quoi vous parlez ici. Les conteneurs IoC examinent-ils les propriétés privées dans le cadre de leur implémentation interne? Je ne sais pas pourquoi ils le feraient, mais encore une fois, s'ils le font, ce n'est pas votre souci de savoir comment une bibliothèque que vous utilisez est mise en œuvre.

    Ou, peut-être, ils exposent 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 vraiment une caractéristique commune. Mais même si c'est là, c'est un cas simple d'un outil qui peut être mal utilisé. N'oubliez pas que même sans conteneur IoC, ce ne sont que quelques lignes de code Reflection pour accéder et modifier une propriété privée. C'est quelque chose que vous ne devriez presque jamais faire, mais cela ne signifie pas que .NET est mauvais pour exposer la capacité. Si quelqu'un abuse manifestement et sauvagement d'un outil, c'est la faute de la personne, pas celle de l'outil.

  3. Le point ultime ici est similaire à 2. La grande majorité du temps, les conteneurs IoC utilisent le constructeur! L'injection Setter est offerte pour des circonstances très spécifiques où , pour des raisons particulières, l'injection de constructeur ne peut pas être utilisée. Quiconque utilise l'injection de setter tout le temps pour masquer le nombre de dépendances transmises est en train d'absoudre massivement 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, vous auriez peut-être un point. Ce serait comme rendre public chaque membre de ma classe, puis blâmer les autres quand ils modifient des choses qu'ils ne devraient pas, non? Mais quiconque utilise l'injection de setter pour couvrir les violations de SRP ignore délibérément ou ignore complètement les principes de conception de base. Il est déraisonnable de blâmer cela sur le conteneur IoC.

    C'est particulièrement vrai parce que c'est aussi quelque chose que vous pouvez faire aussi facilement avec l'ID du pauvre:

    var myObject 
       = new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
    

    Donc, vraiment, cette inquiétude semble complètement orthogonale à l'utilisation ou non d'un conteneur IoC.

  4. 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 tel était le cas, ils ne seraient pas tous les deux dans le même acronyme SOLID!

    De plus, ni les points a) ni b) ne sont proches d'avoir quelque chose à voir avec l'OCP. Je ne sais même pas comment y répondre en ce qui concerne l'OCP.

    La seule chose que je peux deviner, c'est que vous pensez qu'OCP est lié au fait que le comportement n'est pas modifié au moment de l'exécution, ou à partir de quel endroit du code le cycle de vie des dépendances est contrôlé. Ce n'est pas. OCP consiste à ne pas avoir à modifier le code existant lorsque des exigences sont ajoutées ou changées. Il s'agit de écrire du code , pas de la façon dont le code que vous avez déjà écrit est collé ensemble (bien que, bien sûr, un couplage lâche soit une partie importante de la réalisation OCP).

Et une dernière chose que vous dites:

Mais je ne peux pas faire confiance à un outil tiers qui modifie mon code compilé pour effectuer des solutions de contournement à des choses que je ne pourrais pas faire manuellement.

Oui, vous pouvez . Il n'y a absolument aucune raison pour vous 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 sardoniquement que les conteneurs IoC "n'utilisent pas une technique secrète dont nous n'avons jamais entendu parler" pour éviter le code désordonné et sujet à la duplication pour construire un graphique de dépendance. Et vous avez tout à fait raison, ce qu'ils font, c'est réellement aborder ces choses avec les mêmes techniques de base que nous, les programmeurs, faisons toujours.

Permettez-moi de vous parler d'un scénario hypothétique. En tant que programmeur, vous avez assemblé une grande application, et au point d'entrée, où vous construisez votre graphique d'objet, vous remarquez que vous avez un code assez désordonné. Il y a pas mal de classes qui sont utilisées encore et encore, et chaque fois que vous en créez une, vous devez à nouveau construire toute la chaîne de dépendances sous elles. De plus, vous constatez que vous n'avez aucun moyen expressif de déclarer ou de contrôler le cycle de vie des dépendances, sauf avec un code personnalisé pour chacune. Votre code est non structuré et plein de répétitions. C'est le désordre dont vous parlez dans votre paragraphe d'introduction.

Donc, tout d'abord, vous commencez à refactoriser un peu - où du code répété est suffisamment structuré, vous le retirez dans des méthodes d'assistance, etc. Mais alors vous commencez à penser - est-ce un problème que je pourrais peut-être aborder dans un sens général , qui n'est pas spécifique à ce projet particulier mais pourrait vous aider dans tous vos projets futurs?

Alors, asseyez-vous, réfléchissez et décidez qu'il devrait y avoir une classe capable de résoudre les dépendances. Et vous décrivez de quelles méthodes publiques 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 paramètre supplémentaire singleton indique s'il faut utiliser la même instance de concreteType à chaque fois, ou toujours en créer une nouvelle.

Resolve essaiera simplement de construire T avec n'importe quel constructeur qu'il pourra trouver dont les arguments sont tous de types qui ont été précédemment liés. Il peut également s'appeler récursivement pour résoudre les dépendances tout en bas. S'il ne peut pas résoudre une instance car tout n'est pas 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 de mise en cache pour les liaisons où singleton est vrai, mais certainement rien de radical ni d'horrible. Et une fois que vous avez terminé - voila , vous avez le cœur de votre propre conteneur IoC! Est-ce vraiment effrayant? La seule vraie différence entre cela 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 en son cœur, ce que vous avez là est l'essence d'un conteneur IoC.

35
Ben Aaronson

Les conteneurs IOC ne brisent-ils pas beaucoup de ces principes?

En théorie? Non. Les conteneurs IoC ne sont qu'un outil. Vous pouvez avoir des objets qui ont une seule responsabilité, des interfaces bien séparées, ne peuvent pas voir leur comportement modifié une fois écrit, etc.

En pratique? Absolument.

Je veux dire, j'ai entendu pas mal de programmeurs dire qu'ils utilisent des conteneurs IoC spécifiquement pour vous permettre d'utiliser une configuration pour changer le comportement du système - violer le principe ouvert/fermé. Il y avait des blagues sur le codage 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 pour tous les conteneurs IoC) est un moyen rapide et facile de créer du code hautement couplé et très fragile.

Regardons les choses différemment. Prenons une classe très basique qui a une seule dépendance de base. Cela dépend d'un Action, d'un délégué ou d'un foncteur qui ne prend aucun paramètre et retourne void. Quelle est la responsabilité de cette classe? Tu ne peux pas le dire. Je pourrais nommer cette dépendance quelque chose de clair comme CleanUpAction qui vous fait penser qu'elle fait ce que vous voulez qu'elle fasse. Dès que l'IoC entre en jeu, il devient n'importe quoi . Tout le monde peut entrer dans la configuration et modifier votre logiciel pour faire des choses arbitraires.

"En quoi est-ce différent de passer un Action au constructeur?" tu demandes. C'est différent parce que vous ne pouvez pas changer ce qui est passé dans le constructeur une fois le logiciel déployé (ou à l'exécution!). C'est différent parce que vos tests unitaires (ou les tests unitaires de vos consommateurs) ont couvert les scénarios où le délégué est passé dans le constructeur.

"Mais c'est un faux exemple! Mes affaires ne permettent pas de changer de comportement!" vous exclamez-vous. Désolé de vous le dire, mais c'est le cas. À moins que l'interface que vous injectez ne soit juste des données. Et votre interface n'est pas seulement des données, car s'il ne s'agissait que de données, vous construisez un objet simple et l'utilisez. Dès que vous avez une méthode virtuelle dans votre point d'injection, le contrat de votre classe utilisant IoC est "je peux faire n'importe quoi ".

"En quoi est-ce différent de l'utilisation de scripts pour piloter des choses? C'est parfaitement bien." certains d'entre vous demandent. Ce n'est pas différent. Du point de vue de la conception d'un programme, il n'y a pas de différence. Vous appelez hors de votre programme pour exécuter du code arbitraire. Et bien sûr, cela a été fait dans les jeux depuis des lustres. Cela se fait dans des tonnes d'outils d'administration de systèmes. Est-ce OOP? Pas vraiment.

Il n'y a aucune excuse pour leur omniprésence actuelle. Les conteneurs IoC ont un objectif solide: coller vos dépendances lorsque vous en avez tant de combinaisons, ou qu'elles se déplacent si rapidement qu'il est impossible de rendre ces dépendances explicites. Donc, très peu d'entreprises correspondent à ce scénario.

27
Telastyn

L'utilisation d'un conteneur IoC viole-t-elle nécessairement les principes OOP/SOLID? Non.

Pouvez-vous utiliser des conteneurs IoC de telle manière qu'ils violent les principes de la POO/SOLIDE? Oui.

Les conteneurs IoC ne sont que des cadres de gestion des dépendances dans votre application.

Vous avez déclaré des injections paresseuses, des fixateurs de propriétés et quelques autres fonctionnalités que je n'ai pas encore ressenties le besoin d'utiliser, avec lesquelles je suis d'accord peuvent vous donner une conception moins qu'optimale. Cependant, l'utilisation de ces fonctionnalités spécifiques est due aux limitations de la technologie dans laquelle vous implémentez les 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 avec des paradigmes stricts OOP en tête).

ASP.NET MVC d'autre part, il est tout à fait possible d'utiliser un conteneur IoC en utilisant l'injection de constructeur très conviviale OOP/SOLID.

Dans ce scénario (à l'aide de l'injection de constructeur), je crois qu'il favorise 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'exister. L'utilisation d'un conteneur IoC facilite leur organisation.

  • Ouvert/Fermé - C'est là que l'utilisation d'un conteneur IoC brille vraiment, vous pouvez étendre les fonctionnalités d'une classe en injectant différentes dépendances. Le fait d'avoir des dépendances que vous injectez vous permet de modifier le comportement de cette classe. Un bon exemple est de changer la classe que vous injectez en écrivant un décorateur pour dire ajouter la mise en cache ou la journalisation. Vous pouvez modifier complètement 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égrégation des interfaces - Vous pouvez attribuer plusieurs interfaces à un seul moment concret si vous le souhaitez, tout conteneur IoC digne d'un grain de sel le fera facilement.

  • Inversion de dépendance - Les conteneurs IoC vous permettent de faire facilement l'inversion de dépendance.

Vous définissez un tas de dépendances et déclarez les relations et les étendues, et bing bang boom: vous avez comme par magie un addon qui modifie votre code ou I.L. à un moment donné et fait fonctionner les choses. Ce n'est ni explicite ni transparent.

Il est à la fois explicite et transparent, vous devez indiquer à votre conteneur IoC comment câbler les dépendances et comment 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.

Je suis d'accord que lorsque j'écris ou lis du code en utilisant des conteneurs IOC, il se débarrasse d'un code légèrement inélégant.

C'est la raison pour laquelle OOP/SOLID rend les systèmes gérables. L'utilisation d'un conteneur IoC est conforme à cet objectif.

L'auteur de cette classe dit: "Si nous n'utilisions pas de conteneur IOC, le constructeur aurait une douzaine de paramètres !!! C'est horrible!"

Une classe avec 12 dépendances est probablement une violation de SRP, quel que soit le contexte dans lequel vous l'utilisez.

14
Matthew

Est-ce que IOC ne cassent pas et ne permettent pas aux codeurs de casser beaucoup de principes OOP/SOLID?

Le mot clé static en C # permet aux développeurs de briser un grand nombre de principes OOP/SOLID mais c'est toujours un outil valide dans les bonnes circonstances.

Les conteneurs IoC permettent un code bien structuré et réduisent la charge cognitive pour les développeurs. Un conteneur IoC peut être utilisé pour rendre les principes SOLID beaucoup plus faciles. Il peut être mal utilisé, bien sûr, mais si nous excluions l'utilisation de tout outil pouvant être utilisé à mauvais escient, nous devions abandonner la programmation. tout à fait.

Donc, bien que cette diatribe soulève des points valables sur le code mal écrit, ce n'est pas exactement une question et ce n'est pas exactement utile.

5
Stephen

Je vois plusieurs erreurs et malentendus dans votre question. Le premier problème majeur est qu'il vous manque une distinction entre l'injection de propriété/champ, l'injection de constructeur et le localisateur de service. Il y a eu beaucoup de discussions (par exemple, des guerres de flammes) sur la bonne façon de faire les choses. Dans votre question, vous semblez supposer uniquement l'injection de propriété et ignorer l'injection de constructeur.

La deuxième chose est que 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 modifier la conception de votre code si vous utilisez l'IoC. Vos problèmes avec les classes avec de nombreux constructeurs sont le cas d'IoC cachant de vrais problèmes avec votre code (très probablement une classe qui a des interruptions 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ù se situe le problème du principe ouvert-fermé, donc je ne commenterai pas à ce sujet. Mais il vous manque une partie de SOLID, qui a une influence majeure sur cela: le 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 vous retrouverez avec beaucoup de classes, qui nécessitent plusieurs interfaces pour être réalisées et fournies lors de la construction pour fonctionner correctement. Si vous deviez ensuite 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 modifier sans avoir à recompiler le programme. Vous confondez probablement le principe ouvert/fermé avec le principe d'inversion de dépendance dans votre question.

Donc, la réponse à votre question est non. Les conteneurs IoC ne cassent pas OOP. Bien au contraire, ils résolvent les problèmes causés par les principes SOLID (en particulier le principe d'inversion de dépendance)).

3
Euphoric