Si la réponse de Mark est excellente pour les scénarios web, le principal défaut de son application à toutes les architectures (à savoir les clients riches - c'est-à-dire WPF, WinForms, iOS, etc.) est l'hypothèse selon laquelle tous les composants nécessaires à une opération peuvent/doivent être créés en une seule fois.
Pour les serveurs web, cela est logique puisque chaque requête est extrêmement brève et qu'un contrôleur ASP.NET MVC est créé par le framework sous-jacent (sans code utilisateur) pour chaque requête entrante. Ainsi, le contrôleur et toutes ses dépendances peuvent facilement être composés par un framework DI, et le coût de maintenance est très faible. Notez que le framework web est responsable de la gestion de la durée de vie du contrôleur et, à toutes fins utiles, de la durée de vie de toutes ses dépendances (que le framework DI créera/injectera pour vous lors de la création du contrôleur). Il est tout à fait normal que les dépendances vivent pendant toute la durée de la requête et que votre code utilisateur n'ait pas besoin de gérer lui-même la durée de vie des composants et sous-composants. Notez également que les serveurs Web sont apatrides entre les différentes requêtes (à l'exception de l'état de la session, mais cela n'est pas pertinent pour cette discussion) et que vous n'avez jamais plusieurs instances de contrôleur/enfant-contrôleur qui doivent vivre en même temps pour servir une seule requête.
Dans les applications client riche, cependant, ce n'est pas du tout le cas. Si vous utilisez une architecture MVC/MVVM (ce que vous devriez faire !), la session d'un utilisateur est durable et les contrôleurs créent des sous-contrôleurs / contrôleurs frères au fur et à mesure que l'utilisateur navigue dans l'application (voir la note sur MVVM en bas de page). L'analogie avec le monde du web est que chaque entrée de l'utilisateur (clic sur un bouton, opération effectuée) dans une application client riche équivaut à une requête reçue par le framework web. La grande différence, cependant, est que vous voulez que les contrôleurs d'une application client riche restent en vie entre les opérations (il est très possible que l'utilisateur effectue plusieurs opérations sur le même écran - ce qui est régi par un contrôleur particulier) et que les sous-contrôleurs soient créés et détruits au fur et à mesure que l'utilisateur effectue différentes actions (pensez à un contrôle d'onglet qui crée paresseusement l'onglet si l'utilisateur navigue vers lui, ou à un élément d'interface utilisateur qui ne doit être chargé que si l'utilisateur effectue des actions particulières sur un écran).
Ces deux caractéristiques signifient que c'est le code utilisateur qui doit gérer la durée de vie des contrôleurs/sous-contrôleurs, et que les dépendances des contrôleurs ne doivent PAS toutes être créées en amont. (c'est-à-dire les sous-contrôleurs, les modèles de vue, les autres composants de présentation, etc.) Si vous utilisez un cadre d'intégration pour assumer ces responsabilités, vous vous retrouverez non seulement avec beaucoup plus de code là où il ne doit pas être (voir : Anti-modèle de surinjection de constructeur ) mais vous devrez également transmettre un conteneur de dépendances dans la majeure partie de votre couche de présentation afin que vos composants puissent l'utiliser pour créer leurs sous-composants lorsque cela est nécessaire.
Pourquoi est-ce mauvais que mon code utilisateur ait accès au conteneur DI ?
1) Le conteneur de dépendances contient des références à un grand nombre de composants de votre application. Passer ce mauvais garçon à chaque composant qui a besoin de créer/gérer un sous-composant est l'équivalent de l'utilisation de globales dans votre architecture. Pire encore, tout sous-composant peut également enregistrer de nouveaux composants dans le conteneur, de sorte qu'il deviendra rapidement un stockage global. Les développeurs introduiront des objets dans le conteneur uniquement pour faire circuler des données entre les composants (soit entre contrôleurs frères, soit entre des hiérarchies de contrôleurs profondes - par exemple, un contrôleur ancêtre doit récupérer des données d'un contrôleur grand-parent). Notez que dans le monde du web, où le conteneur n'est pas transmis au code utilisateur, ce n'est jamais un problème.
2) L'autre problème avec les conteneurs de dépendances par rapport aux localisateurs de services / usines / instanciation directe d'objets est que la résolution à partir d'un conteneur rend complètement ambiguë la question de savoir si vous CREEZ un composant ou si vous REUTILISER simplement un composant existant. Au lieu de cela, c'est à une configuration centralisée (c'est-à-dire un bootstrapper / Composition Root) de déterminer la durée de vie du composant. Dans certains cas, cela ne pose pas de problème (par exemple, les contrôleurs Web, où ce n'est pas le code utilisateur qui doit gérer la durée de vie du composant, mais le cadre de traitement des demandes d'exécution lui-même). Cette situation est toutefois extrêmement problématique lorsque la conception de vos composants doit INDIQUER s'il est de leur responsabilité de gérer un composant et quelle doit être sa durée de vie (exemple : Une application téléphonique fait apparaître une feuille qui demande à l'utilisateur quelques informations. Pour ce faire, un contrôleur crée un sous-contrôleur qui gère la feuille superposée. Une fois que l'utilisateur a entré des informations, la feuille est résiliée et le contrôle est renvoyé au contrôleur initial, qui conserve toujours l'état de ce que l'utilisateur faisait auparavant). Si DI est utilisé pour résoudre le sous-contrôleur de la feuille, il est ambigu de savoir quelle doit être sa durée de vie ou qui doit être responsable de sa gestion (le contrôleur initial). Comparez cela à la responsabilité explicite dictée par l'utilisation d'autres mécanismes.
Scénario A :
// not sure whether I'm responsible for creating the thing or not
DependencyContainer.GimmeA<Thing>()
Scénario B :
// responsibility is clear that this component is responsible for creation
Factory.CreateMeA<Thing>()
// or simply
new Thing()
Scénario C :
// responsibility is clear that this component is not responsible for creation, but rather only consumption
ServiceLocator.GetMeTheExisting<Thing>()
// or simply
ServiceLocator.Thing
Comme vous pouvez le constater, la DI ne précise pas clairement qui est responsable de la gestion de la durée de vie du sous-composant.
NOTE : Techniquement parlant, de nombreux frameworks DI ont un moyen de créer des composants paresseux. composants paresseusement (voir : Comment ne pas faire l'injection de dépendances - le conteneur statique ou singleton ), ce qui est bien mieux que de passer le conteneur, mais vous devez quand même payer le coût de la mutation de votre code pour faire passer des fonctions de création partout, vous n'avez pas de support de premier niveau pour passer des paramètres de constructeur valides pendant la création. pour passer des paramètres de constructeur valides pendant la création, et à la fin de la journée, vous utilisez toujours un mécanisme d'indirection inutilement dans des endroits où le seul avantage est d'atteindre la testabilité, ce qui peut être réalisé de manière plus simple et plus efficace (voir ci-dessous).
Que signifie tout cela ?
Cela signifie que le DI est approprié pour certains scénarios, et inapproprié pour d'autres. Dans les applications client riche, il se trouve qu'elle présente beaucoup des inconvénients de l'ID et très peu des avantages. Plus la complexité de votre application augmente, plus les coûts de maintenance augmentent. Elle comporte également un grave risque d'utilisation abusive qui, en fonction de la rigueur de la communication au sein de l'équipe et des processus de révision du code, peut aller d'une absence de problème à une grave dette technologique. Il existe un mythe selon lequel les localisateurs de services, les fabriques ou la bonne vieille instanciation sont en quelque sorte des mécanismes mauvais et dépassés, simplement parce qu'ils ne sont peut-être pas le mécanisme optimal dans le monde des applications Web, où peut-être beaucoup de gens jouent. Nous ne devrions pas trop généraliser ces apprentissages à tous les scénarios et tout considérer comme des clous simplement parce que nous avons appris à manier un marteau particulier.
Ma recommandation POUR LES APPLICATIONS CLIENT RICHE est d'utiliser le mécanisme minimal qui répond aux exigences de chaque composant à disposition. Dans 80 % des cas, il s'agit d'une instanciation directe. Les localisateurs de services peuvent être utilisés pour héberger les principaux composants de la couche métier (c'est-à-dire les services d'application qui sont généralement de nature singleton), et bien sûr, les fabriques et même le modèle Singleton ont également leur place. Rien ne vous empêche d'utiliser un cadre DI caché derrière votre localisateur de services pour créer les dépendances de votre couche métier et tout ce dont elles dépendent en une seule fois - si cela finit par vous faciliter la vie dans cette couche, et si cette couche ne présente pas le chargement paresseux que les couches de présentation de clients riches font massivement. . Veillez simplement à protéger votre code utilisateur contre l'accès à ce conteneur afin d'éviter le désordre que peut créer le passage d'un conteneur DI.
Qu'en est-il de la testabilité ?
La testabilité peut absolument être réalisée sans cadre de DI. Je recommande d'utiliser un cadre d'interception tel que UnitBox (gratuit) ou TypeMock (coûteux). Ces frameworks vous donnent les outils dont vous avez besoin pour contourner le problème en question (comment simuler l'instanciation et les appels statiques en C#) et ne vous obligent pas à modifier toute votre architecture pour les contourner (ce qui est malheureusement la tendance dans le monde .NET/Java). Il est plus sage de trouver une solution au problème qui se pose et d'utiliser les mécanismes et les modèles de langage naturel optimaux pour le composant sous-jacent que d'essayer de faire entrer chaque cheville carrée dans le trou rond de DI. Une fois que vous aurez commencé à utiliser ces mécanismes plus simples et plus spécifiques, vous constaterez que le DI n'est que très peu nécessaire dans votre base de code, voire pas du tout.
NOTE : Pour les architectures MVVM
Dans les architectures MVVM de base, les modèles de vue assument effectivement la responsabilité des contrôleurs. responsabilité des contrôleurs, donc, à toutes fins utiles, considérez que la formulation considérer que la formulation "contrôleur" ci-dessus s'applique à "modèle de vue". MVVM de base fonctionne pour les petites applications, mais lorsque la complexité d'une application augmente, vous pouvez d'utiliser une approche MVCVM. Les modèles de vue deviennent principalement des DTO muets pour pour faciliter la liaison des données à la vue et l'interaction avec la couche couche d'affaires et entre les groupes de modèles de vue représentant les des écrans/sous-écrans est encapsulée dans des composants explicites de composants de contrôleur/sous-contrôleur. Dans l'une ou l'autre architecture, la responsabilité des contrôleurs existe et présente les mêmes caractéristiques discutées ci-dessus.