web-dev-qa-db-fra.com

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

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épendance dans mon application ou devrais-je l'implémenter selon un autre principe?

21
user3223738

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 en .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 autant).

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.

30
Miyamoto Akira

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épondu par @Miyamoto Akira (en particulier la recommandation de lire le livre de Mark Seemann sur "l'injection de dépendance dans .net". Marks blog 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 new dans le 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 l'objectif principal du refactoriste, c'est peut-être là que vous vous arrêtez. Le nouveau code sera écrit avec l'injection du 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 à new dans vos constructeurs sans paramètres par des appels statiques à IOC, essentiellement (ab) 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.

13
Steve

Tout d'abord, je tiens à mentionner que vous vous rendez cela 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 souhaitez les patcher ensemble en injectant des objets réels ou écrire de nouveaux tests unitaires à l'aide de maquettes.

Répétez `` simplement '' 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.

1
Jon

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.

0
Low Flying Pelican

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 souhaitez l'utiliser, vous configurez généralement l'usine/le constructeur/le conteneur (ou autre) au début de l'application afin qu'il soit clairement visible.

N.B. 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.

0
Robbie Dee

Il n'y a pas de "bonne voie", 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 y a un extension d'usine qui fournit 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.

0
JanDotNet

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 pour vous.

Comme indiqué ci-dessus dans un article précédent, vous souhaitez 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. Sinon rompt le couplage desserré.

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 appels "kernel.Bind<someInterface>().To<someConcreteClass>()" que vous effectuez 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 de intercepter vos appels de constructeur et fournir les instances d'objet dépendant 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 outil sympa 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 construisez cela vous-même, 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

0
Fred