Quelle est la «bonne» façon d'implémenter DI dans .NET?

22

Je cherche à implémenter l'injection de dépendances dans une application relativement grande, mais je n'ai aucune expérience en la matière. J'ai étudié le concept et quelques implémentations d'IoC et d'injecteurs de dépendances disponibles, comme Unity et Ninject. Cependant, il y a une chose qui m'échappe. Comment dois-je organiser la création d'instances dans mon application?

Ce à quoi je pense, c'est que je peux créer quelques usines spécifiques qui contiendront la logique de création d'objets pour quelques types de classes spécifiques. Fondamentalement, une classe statique avec une méthode appelant la méthode Ninject Get () d'une instance de noyau statique dans cette classe.

Sera-ce une approche correcte de l'implémentation de l'injection de dépendances dans mon application ou devrais-je l'implémenter selon un autre principe?

user3223738
la source
5
Je ne pense pas qu'il y ait la bonne voie, mais beaucoup de bonnes façons, selon votre projet. J'adhérerais aux autres et suggérerais l'injection de constructeur, puisque vous pourrez vous assurer que chaque dépendance est injectée sur un seul point. De plus, si les signatures des constructeurs s'allongent trop longtemps, vous saurez que les classes en font trop.
Paul Kertscher
Il est difficile de répondre sans savoir quel type de projet .net vous construisez. Une bonne réponse pour WPF pourrait être une mauvaise réponse pour MVC, par exemple.
JMK
Il est agréable d'organiser tous les enregistrements de dépendances dans un module DI pour la solution ou pour chaque projet, et éventuellement un pour certains tests en fonction de la profondeur que vous souhaitez tester. Oh oui, bien sûr, vous devriez utiliser l'injection de constructeur, les autres trucs sont pour des utilisations plus avancées / folles.
Mark Rogers

Réponses:

30

Ne pensez pas encore à l'outil que vous allez utiliser. Vous pouvez faire DI sans conteneur IoC.

Premier point: Mark Seemann a un très bon livre sur DI dans .Net

Deuxième: racine de composition. Assurez-vous que toute la configuration se fait au point d'entrée du projet. Le reste de votre code doit connaître les injections, pas les outils utilisés.

Troisièmement: l'injection de constructeur est la voie la plus probable (il y a des cas où vous ne le voudriez pas, mais pas beaucoup).

Quatrièmement: étudier l'utilisation des usines lambda et d'autres fonctionnalités similaires pour éviter de créer des interfaces / classes inutiles dans le seul but de l'injection.

Miyamoto Akira
la source
5
Tous d'excellents conseils; en particulier la première partie: apprenez à faire de la DI pure, puis commencez à regarder les conteneurs IoC qui peuvent réduire la quantité de code passe-partout requise par cette approche.
David Arno
6
... ou ignorez tous les conteneurs IoC - pour conserver tous les avantages de la vérification statique.
Den
J'aime que ce conseil soit un bon point de départ, avant d'entrer dans DI, et j'ai en fait le livre de Mark Seemann.
Snoop
Je peux appuyer cette réponse. Nous avons utilisé avec succès pauvres-mans-DI (bootstrapper manuscrite) dans une application assez grande en déchirant des parties de la logique de bootstrapper dans les modules qui composaient l'application.
wigy
1
Faites attention. L'utilisation d'injections lambda peut être une voie rapide vers la folie, en particulier du type des dommages de conception induits par les tests. Je sais. J'ai emprunté cette voie.
jpmc26
13

Votre question comporte deux parties: comment implémenter DI correctement et comment refactoriser une grande application pour utiliser DI.

La première partie est bien répondue par @Miyamoto Akira (en particulier la recommandation de lire le livre de Mark Seemann sur "l'injection de dépendance dans .net". Le blog Marks est également une bonne ressource gratuite.

La deuxième partie est beaucoup plus compliquée.

Une bonne première étape serait de simplement déplacer toute l'instanciation dans les constructeurs de classes - pas d'injecter les dépendances, juste de vous assurer que vous n'appelez que newle constructeur.

Cela mettra en évidence toutes les violations de SRP que vous avez commises, afin que vous puissiez commencer à diviser la classe en petits collaborateurs.

Le prochain problème que vous trouverez sera les classes qui dépendent des paramètres d'exécution pour la construction. Vous pouvez généralement résoudre ce problème en créant des usines simples, souvent avec Func<param,type>, en les initialisant dans le constructeur et en les appelant dans les méthodes.

La prochaine étape serait de créer des interfaces pour vos dépendances et d'ajouter un deuxième constructeur à vos classes à l'exception de ces interfaces. Votre constructeur sans paramètre renouvelle les instances concrètes et les transmet au nouveau constructeur. Ceci est communément appelé «B * stard Injection» ou «Poor mans DI».

Cela vous donnera la possibilité de faire des tests unitaires, et si c'était le principal objectif du refactoriste, c'est peut-être là que vous vous arrêtez. Le nouveau code sera écrit avec l'injection de constructeur, mais votre ancien code peut continuer à fonctionner tel qu'il est écrit mais toujours testable.

Vous pouvez bien sûr aller plus loin. Si vous avez l'intention d'utiliser un conteneur IOC, alors une étape suivante pourrait être de remplacer tous les appels directs à newdans vos constructeurs sans paramètres par des appels statiques au conteneur IOC, essentiellement (ab) en l'utilisant comme localisateur de service.

Cela générera plus de cas de paramètres de constructeur d'exécution à traiter comme auparavant.

Une fois cela fait, vous pouvez commencer à supprimer les constructeurs sans paramètres et à refactoriser la DI pure.

En fin de compte, cela va être beaucoup de travail, alors assurez-vous de décider pourquoi vous voulez le faire, et priorisez les parties de la base de code qui bénéficieront le plus du refactor.

Steve
la source
3
Merci pour une réponse très élaborée. Vous m'avez donné quelques idées sur la façon d'aborder le problème auquel je suis confronté. L'architecture complète de l'application est déjà conçue avec IoC à l'esprit. La principale raison pour laquelle je veux utiliser DI n'est même pas le test unitaire, il s'agit d'un bonus mais plutôt d'une capacité à échanger différentes implémentations pour différentes interfaces, définies dans le cœur de mon application, avec le moins d'effort possible. L'application en question fonctionne dans un environnement en constante évolution et je dois souvent échanger des parties de l'application pour utiliser de nouvelles implémentations en fonction des changements de l'environnement.
user3223738
1
Heureux d'avoir pu aider, et oui, je suis d'accord que le plus grand avantage de la DI est le couplage lâche et la capacité de reconfigurer facilement que cela apporte, les tests unitaires étant un bel effet secondaire.
Steve
1

Tout d'abord, je tiens à mentionner que vous vous rendez la tâche beaucoup plus difficile en refactorisant un projet existant plutôt qu'en démarrant un nouveau projet.

Vous avez dit qu'il s'agissait d'une grande application, alors choisissez un petit composant pour commencer. De préférence un composant «nœud-feuille» qui n'est utilisé par rien d'autre. Je ne sais pas quel est l'état des tests automatisés sur cette application, mais vous casserez tous les tests unitaires pour ce composant. Alors préparez-vous à cela. L'étape 0 consiste à écrire des tests d'intégration pour le composant que vous modifierez s'ils n'existent pas déjà. En dernier recours (pas d'infrastructure de test; pas d'adhésion pour l'écrire), déterminez une série de tests manuels que vous pouvez faire pour vérifier que ce composant fonctionne.

La façon la plus simple de définir votre objectif pour le refactoriseur DI est de supprimer toutes les instances du "nouvel" opérateur de ce composant. Ceux-ci se répartissent généralement en deux catégories:

  1. Variable membre invariante: ce sont des variables qui sont définies une fois (généralement dans le constructeur) et ne sont pas réaffectées pour la durée de vie de l'objet. Pour ceux-ci, vous pouvez injecter une instance de l'objet dans le constructeur. Vous n'êtes généralement pas responsable de l'élimination de ces objets (je ne veux pas dire jamais ici, mais vous ne devriez vraiment pas avoir cette responsabilité).

  2. Variable membre variable / méthode variable: Ce sont des variables qui récupèreront les ordures à un moment donné pendant la durée de vie de l'objet. Pour ceux-ci, vous voudrez injecter une usine dans votre classe pour fournir ces instances. Vous êtes responsable de l'élimination des objets créés par une usine.

Votre conteneur IoC (ninject on dirait) prendra la responsabilité d'instancier ces objets et de mettre en œuvre vos interfaces d'usine. Tout ce qui utilise le composant que vous avez modifié devra connaître le conteneur IoC pour qu'il puisse récupérer votre composant.

Une fois que vous avez terminé ce qui précède, vous pourrez profiter de tous les avantages que vous espérez obtenir de DI dans votre composant sélectionné. Ce serait le bon moment pour ajouter / corriger ces tests unitaires. S'il y avait des tests unitaires existants, vous devrez décider si vous voulez les patcher ensemble en injectant des objets réels ou écrire de nouveaux tests unitaires à l'aide de maquettes.

'Simplement', répétez ce qui précède pour chaque composant de votre application, en déplaçant la référence vers le conteneur IoC au fur et à mesure jusqu'à ce que seul le principal ait besoin de le savoir.

Jon
la source
1
Bon conseil: commencez par un petit composant plutôt que par la «GRANDE réécriture»
Daniel Hollinrake
0

L'approche correcte consiste à utiliser l'injection de constructeur, si vous utilisez

Ce à quoi je pense, c'est que je peux créer quelques usines spécifiques qui contiendront la logique de création d'objets pour quelques types de classes spécifiques. Fondamentalement, une classe statique avec une méthode appelant la méthode Ninject Get () d'une instance de noyau statique dans cette classe.

alors vous vous retrouvez avec localisateur de service, que l'injection de dépendance.

Pélican volant à basse altitude
la source
Sûr. Injection constructeur. Disons que j'ai une classe qui accepte une implémentation d'interface comme l'un des arguments. Mais j'ai encore besoin de créer une instance de l'implémentation de l'interface et de la transmettre à ce constructeur quelque part. De préférence, il devrait s'agir d'un morceau de code centralisé.
user3223738
2
Non, lorsque vous initialisez le conteneur DI, vous devez spécifier l'implémentation des interfaces, et le conteneur DI créera l'instance et l'injectera au constructeur.
Low Flying Pelican
Personnellement, je trouve que l'injection de constructeur est surutilisée. Je l'ai vu trop souvent que 10 services différents sont injectés et qu'un seul est réellement nécessaire pour un appel de fonction - pourquoi ne fait-il pas alors partie de l'argument de fonction?
urbanhusky
2
Si 10 services différents sont injectés, c'est parce que quelqu'un viole SRP, qui devrait être divisé en composants plus petits.
Low Flying Pelican
1
@Fabio La question est de savoir ce que cela vous rapporterait. Je n'ai pas encore vu d'exemple où avoir une classe géante qui traite d'une douzaine de choses totalement différentes est une bonne conception. La seule chose que DI fait est de rendre toutes ces violations plus évidentes.
Voo
0

Vous dites que vous voulez l'utiliser mais ne dites pas pourquoi.

DI n'est rien d'autre que fournir un mécanisme pour générer des concrétions à partir d'interfaces.

Cela vient en soi du DIP . Si votre code est déjà écrit dans ce style et que vous avez un seul endroit où des concrétions sont générées, DI n'apporte plus rien à la fête. L'ajout de code de cadre DI ici serait tout simplement gonflé et obscurcir votre base de code.

En supposant que vous ne voulez l'utiliser, vous généralement mis en place l'usine / constructeur / conteneur (ou autre) au début de l'application il est clairement visible.

NB il est très facile de rouler le vôtre si vous le souhaitez plutôt que de vous engager sur Ninject / StructureMap ou autre. Si toutefois vous avez un roulement raisonnable de personnel, il peut graisser les roues pour utiliser un cadre reconnu ou au moins l'écrire dans ce style afin qu'il ne soit pas trop une courbe d'apprentissage.

Robbie Dee
la source
0

En fait, la "bonne" façon est de ne PAS utiliser d'usine du tout, sauf s'il n'y a absolument pas d'autre choix (comme dans les tests unitaires et certaines simulations - pour le code de production, vous n'utilisez PAS d'usine)! Cela est en fait un anti-modèle et doit être évité à tout prix. Tout l'intérêt d'un conteneur DI est de permettre au gadget de faire le travail pour vous.

Comme indiqué ci-dessus dans un article précédent, vous voulez que votre gadget IoC assume la responsabilité de la création des différents objets dépendants dans votre application. Cela signifie laisser votre gadget DI créer et gérer les différentes instances lui-même. C'est tout le point derrière DI - vos objets ne doivent JAMAIS savoir comment créer et / ou gérer les objets dont ils dépendent. Pour faire autrement pauses lâche accouplement.

La conversion d'une application existante en toutes DI est une étape énorme, mais en mettant de côté les difficultés évidentes à le faire, vous voudrez également (juste pour vous faciliter la vie) explorer un outil DI qui exécutera automatiquement la majeure partie de vos liaisons (le noyau de quelque chose comme Ninject est les "kernel.Bind<someInterface>().To<someConcreteClass>()"appels que vous faites pour faire correspondre vos déclarations d'interface aux classes concrètes que vous souhaitez utiliser pour implémenter ces interfaces. Ce sont ces appels "Bind" qui permettent à votre gadget DI d'intercepter vos appels de constructeur et de fournir les instances d'objets dépendantes nécessaires. Un constructeur typique (pseudo-code montré ici) pour une classe peut être:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Notez que nulle part dans ce code, aucun code n'a créé / géré / publié l'instance de SomeConcreteClassA ou SomeOtherConcreteClassB. En fait, aucune classe concrète n'était même référencée. Alors ... où la magie s'est-elle produite?

Dans la partie de démarrage de votre application, les événements suivants ont eu lieu (encore une fois, il s'agit d'un pseudo-code mais il est assez proche de la réalité (Ninject) ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Ce petit bout de code indique au gadget Ninject de rechercher des constructeurs, de les analyser, de rechercher des instances d'interfaces qu'il a été configuré pour gérer (c'est-à-dire les appels "Bind"), puis de créer et de remplacer une instance de la classe concrète partout où l'instance est référencée.

Il existe un bel outil qui complète très bien Ninject, appelé Ninject.Extensions.Conventions (encore un autre package NuGet) qui fera le gros de ce travail pour vous. Ne pas retirer de l'excellente expérience d'apprentissage que vous vivrez au fur et à mesure que vous la construirez, mais pour vous lancer, cela pourrait être un outil pour enquêter.

Si la mémoire est bonne, Unity (officiellement de Microsoft maintenant un projet Open Source) a un appel de méthode ou deux qui font la même chose, d'autres outils ont des assistants similaires.

Quelle que soit la voie que vous choisissez, lisez certainement le livre de Mark Seemann pour l'essentiel de votre formation en DI, cependant, il convient de souligner que même les «grands» du monde de l'ingénierie logicielle (comme Mark) peuvent faire des erreurs flagrantes - Mark a tout oublié Ninject dans son livre alors voici une autre ressource écrite juste pour Ninject. Je l'ai et c'est une bonne lecture: Mastering Ninject for Dependency Injection

Fred
la source
0

Il n'y a pas de «bonne façon», mais il y a quelques principes simples à suivre:

  • Créer la racine de composition au démarrage de l'application
  • Une fois la racine de composition créée, jetez la référence au conteneur / noyau DI (ou au moins l'encapsulez pour qu'elle ne soit pas directement accessible depuis votre application)
  • Ne créez pas d'instances via "nouveau"
  • Passez toutes les dépendances requises en tant qu'abstraction au constructeur

C'est tout. Bien sûr, ce sont des principes et non des lois, mais si vous les suivez, vous pouvez être sûr que vous faites DI (veuillez me corriger si je me trompe).


Alors, comment créer des objets en cours d'exécution sans "nouveau" et sans connaître le conteneur DI?

Dans le cas de NInject, il existe une extension d'usine qui permet la création d'usines. Bien sûr, les usines créées ont toujours une référence interne au noyau, mais celle-ci n'est pas accessible depuis votre application.

JanDotNet
la source