J'essaie de comprendre injections de dépendance (DI), et encore une fois j'ai échoué. Cela semble juste idiot. Mon code n'est jamais un bordel. J'écris à peine des fonctions et des interfaces virtuelles (bien que je le fasse une fois dans une lune bleue) et toute ma configuration est sérialisée de manière magique dans une classe utilisant json.net (parfois à l'aide d'un sérialiseur XML).
Je ne comprends pas très bien quel problème cela résout. Cela ressemble à un moyen de dire: "Salut. Lorsque vous rencontrez cette fonction, retournez un objet de ce type et utilise ces paramètres/données."
Mais ... pourquoi utiliserais-je cela? Remarque Je n'ai jamais eu besoin d'utiliser object
également, mais je comprends à quoi ça sert.
Quelles sont les situations réelles lors de la création d'un site Web ou d'une application de bureau où l'on utiliserait DI? Je peux facilement expliquer pourquoi quelqu'un peut vouloir utiliser des interfaces/fonctions virtuelles dans un jeu, mais il est extrêmement rare (assez rare que je ne me souvienne pas d'une seule instance) de l'utiliser dans du code non-jeu.
Premièrement, je veux expliquer une hypothèse que je pose pour cette réponse. Ce n'est pas toujours vrai, mais assez souvent:
Les interfaces sont des adjectifs; les classes sont des noms.
(En fait, il y a des interfaces qui sont aussi des noms, mais je veux généraliser ici.)
Donc, par exemple une interface peut être quelque chose comme IDisposable
__, IEnumerable
ou IPrintable
. Une classe est une implémentation réelle d'une ou plusieurs de ces interfaces: List
ou Map
peut être une implémentation de IEnumerable
.
Pour comprendre, souvent vos cours dépendent les uns des autres. Par exemple. vous pouvez avoir une classe Database
qui accède à votre base de données (hah, surprise! ;-)), mais vous souhaitez également que cette classe se connecte pour accéder à la base de données. Supposons que vous avez une autre classe Logger
, alors Database
a une dépendance à Logger
.
Jusqu'ici tout va bien.
Vous pouvez modéliser cette dépendance dans votre classe Database
à l'aide de la ligne suivante:
var logger = new Logger();
et tout va bien. Cela ne pose pas de problème jusqu'au jour où vous vous rendez compte que vous avez besoin d'un groupe d'enregistreurs: vous souhaitez parfois vous connecter à la console, parfois au système de fichiers, parfois à l'aide de TCP/IP et d'un serveur de journalisation distant, etc.
Et bien sûr vous PAS voulez changer tout votre code (en attendant, vous en avez des milliards) et remplacer toutes les lignes
var logger = new Logger();
par:
var logger = new TcpLogger();
Tout d'abord, ce n'est pas amusant. Deuxièmement, c'est sujet aux erreurs. Troisièmement, c’est un travail stupide et répétitif pour un singe entraîné. Donc que fais-tu?
Il est évident que c'est une très bonne idée de présenter une interface ICanLog
(ou similaire) implémentée par tous les différents enregistreurs. La première étape de votre code consiste donc à:
ICanLog logger = new Logger();
Maintenant, l'inférence de type ne change plus de type, vous avez toujours une seule interface à développer. La prochaine étape est que vous ne voulez pas avoir new Logger()
encore et encore. Vous mettez donc la fiabilité pour créer de nouvelles instances dans une seule classe de fabrique centrale et vous obtenez un code tel que:
ICanLog logger = LoggerFactory.Create();
L’usine elle-même décide du type d’enregistreur à créer. Votre code n’a plus d’importance, et si vous voulez changer le type d’enregistreur utilisé, vous le changez ne fois: Dans l’usine.
Maintenant, bien sûr, vous pouvez généraliser cette usine et la faire fonctionner pour n'importe quel type:
ICanLog logger = TypeFactory.Create<ICanLog>();
Quelque part, ce TypeFactory a besoin de données de configuration que la classe à instancier lorsqu'un type d’interface spécifique est demandé. Vous avez donc besoin d’un mappage. Bien sûr, vous pouvez faire ce mappage dans votre code, mais un changement de type signifie alors une recompilation. Mais vous pouvez également placer ce mappage dans un fichier XML, par exemple. Cela vous permet de changer la classe réellement utilisée même après la compilation (!), Ce qui signifie dynamiquement, sans recompiler!
Pour vous donner un exemple utile à cet égard: pensez à un logiciel qui ne se connecte pas normalement, mais lorsque votre client appelle et demande de l'aide parce qu'il a un problème, tout ce que vous lui envoyez est un fichier de configuration XML mis à jour. la journalisation est activée et votre support peut utiliser les fichiers journaux pour aider votre client.
Et maintenant, lorsque vous remplacez un peu les noms, vous vous retrouvez avec une implémentation simple de Service Locator, qui est l’un des deux modèles pour Inversion of Control (puisque vous inverser le contrôle sur qui décide quelle classe exacte instancier).
Globalement, cela réduit les dépendances dans votre code, mais maintenant tout votre code a une dépendance par rapport au localisateur de service unique.
injection de dépendance est maintenant la prochaine étape de cette ligne: il suffit de se débarrasser de cette dépendance unique vis-à-vis du localisateur de service: au lieu que diverses classes demandent au localisateur de service une implémentation pour une interface spécifique, - rétablir le contrôle sur qui instancie quoi.
Avec l'injection de dépendance, votre classe Database
a maintenant un constructeur qui nécessite un paramètre de type ICanLog
:
public Database(ICanLog logger) { ... }
Maintenant, votre base de données a toujours un enregistreur à utiliser, mais elle ne sait plus d'où provient cet enregistreur.
Et c'est là qu'intervient un framework DI: vous configurez à nouveau vos mappages, puis demandez à votre framework DI d'instancier votre application pour vous. Étant donné que la classe Application
requiert une implémentation ICanPersistData
, une instance de Database
est injectée, mais elle doit d'abord créer une instance du type de consignataire configuré pour ICanLog
. Etc ...
En résumé, l’injection de dépendances est l’un des deux moyens de supprimer les dépendances dans votre code. C'est très utile pour les changements de configuration après la compilation, et c'est une bonne chose pour les tests unitaires (car il est très facile d'injecter des stubs et/ou des mock).
En pratique, il existe des choses que vous ne pouvez pas faire sans un localisateur de service (par exemple, si vous ne savez pas à l'avance combien d'instances vous avez besoin d'une interface spécifique: un framework DI injecte toujours une seule instance par paramètre, mais vous pouvez appeler un localisateur de service à l’intérieur d’une boucle, bien sûr), c’est pourquoi chaque structure d’ID fournit également un localisateur de service.
Mais au fond, c'est tout.
J'espère que ça t'as aidé.
PS: Ce que j’ai décrit ici est une technique appelée injection de constructeur, il y a aussi injection de propriété où ce ne sont pas les paramètres du constructeur, mais des propriétés qui sont utilisées pour définir et résoudre les dépendances. Considérez l’injection de propriété comme une dépendance optionnelle et l’injection de constructeur comme une dépendance obligatoire. Mais la discussion à ce sujet dépasse le cadre de cette question.
Je pense que bien souvent, les gens s'embrouillent à propos de la différence entre l'injection de dépendance et celle d'une injection de dépendance (ou un conteneur comme on l'appelle souvent).
L'injection de dépendance est un concept très simple. Au lieu de ce code:
public class A {
private B b;
public A() {
this.b = new B(); // A *depends on* B
}
public void DoSomeStuff() {
// Do something with B here
}
}
public static void Main(string[] args) {
A a = new A();
a.DoSomeStuff();
}
vous écrivez un code comme ceci:
public class A {
private B b;
public A(B b) { // A now takes its dependencies as arguments
this.b = b; // look ma, no "new"!
}
public void DoSomeStuff() {
// Do something with B here
}
}
public static void Main(string[] args) {
B b = new B(); // B is constructed here instead
A a = new A(b);
a.DoSomeStuff();
}
Et c'est tout. Sérieusement. Cela vous donne une tonne d'avantages. Deux importantes sont la possibilité de contrôler la fonctionnalité depuis un emplacement central (la fonction Main()
au lieu de la diffuser dans tout votre programme) et la possibilité de tester plus facilement chaque classe en isolation (car vous pouvez passer des simulacres objets dans son constructeur au lieu d’une valeur réelle).
L’inconvénient, bien sûr, est que vous avez maintenant une méga-fonction qui connaît toutes les classes utilisées par votre programme. C'est ce que les cadres DI peuvent aider. Mais si vous avez du mal à comprendre pourquoi cette approche est utile, je vous recommande de commencer par l'injection manuelle de dépendance, afin que vous puissiez mieux comprendre ce que les différents cadres peuvent faire pour vous.
Comme indiqué dans les autres réponses, l’injection de dépendances est un moyen de créer vos dépendances en dehors de la classe qui les utilise. Vous les injectez de l'extérieur et prenez le contrôle de leur création loin de l'intérieur de votre classe. C'est également pourquoi l'injection de dépendance est une réalisation du principe Inversion of control (IoC).
IoC est le principe, où DI est le motif. La raison pour laquelle vous pourriez "avoir besoin de plus d’un enregistreur" n’a jamais été rencontrée, pour autant que je sache, mais c’est parce que vous en avez vraiment besoin, chaque fois que vous testez quelque chose. Un exemple:
Mon article:
Quand je regarde une offre, je veux noter que je l'ai regardée automatiquement, afin de ne pas oublier de le faire.
Vous pourriez tester ceci comme ceci:
[Test]
public void ShouldUpdateTimeStamp
{
// Arrange
var formdata = { . . . }
// System under Test
var weasel = new OfferWeasel();
// Act
var offer = weasel.Create(formdata)
// Assert
offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}
Donc quelque part dans le OfferWeasel
, il vous construit une offre objet comme ceci:
public class OfferWeasel
{
public Offer Create(Formdata formdata)
{
var offer = new Offer();
offer.LastUpdated = DateTime.Now;
return offer;
}
}
Le problème ici est que ce test échouera probablement toujours, car la date qui sera définie sera différente de la date déclarée, même si vous venez de mettre DateTime.Now
dans le code de test, il pourrait être décalé par un couple en millisecondes et échouera donc toujours. Une meilleure solution maintenant serait de créer une interface pour cela, qui vous permette de contrôler quelle heure sera définie:
public interface IGotTheTime
{
DateTime Now {get;}
}
public class CannedTime : IGotTheTime
{
public DateTime Now {get; set;}
}
public class ActualTime : IGotTheTime
{
public DateTime Now {get { return DateTime.Now; }}
}
public class OfferWeasel
{
private readonly IGotTheTime _time;
public OfferWeasel(IGotTheTime time)
{
_time = time;
}
public Offer Create(Formdata formdata)
{
var offer = new Offer();
offer.LastUpdated = _time.Now;
return offer;
}
}
L'interface est l'abstraction. L’une est la réalité et l’autre vous permet de simuler le temps nécessaire. Le test peut alors être modifié comme ceci:
[Test]
public void ShouldUpdateTimeStamp
{
// Arrange
var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
var formdata = { . . . }
var time = new CannedTime { Now = date };
// System under test
var weasel= new OfferWeasel(time);
// Act
var offer = weasel.Create(formdata)
// Assert
offer.LastUpdated.Should().Be(date);
}
Comme cela, vous avez appliqué le principe "d'inversion de contrôle", en injectant une dépendance (obtenir l'heure actuelle). La raison principale est de faciliter les tests unitaires isolés. Il existe d'autres moyens de le faire. Par exemple, une interface et une classe ne sont pas nécessaires ici car en C #, les fonctions peuvent être transmises sous forme de variables. Vous pouvez donc utiliser un Func<DateTime>
au lieu d'une interface pour obtenir le même résultat. Ou, si vous adoptez une approche dynamique, il vous suffit de passer tout objet ayant la méthode équivalente ( saisie manuelle ) et vous n'avez pas du tout besoin d'une interface.
Vous n'aurez presque jamais besoin de plus d'un enregistreur. Néanmoins, l'injection de dépendance est essentielle pour le code de type statique tel que Java ou C #.
Et ... Il convient également de noter qu'un objet ne peut remplir correctement son objectif au moment de l'exécution que si toutes ses dépendances sont disponibles. beaucoup utile dans la mise en place de l'injection de propriété. À mon avis, toutes les dépendances doivent être satisfaites lorsque le constructeur est appelé. L'injection de constructeur est donc la solution.
J'espère que cela a aidé.
Je pense que la réponse classique consiste à créer une application plus découplée, qui ne sait pas quelle implémentation sera utilisée pendant l'exécution.
Par exemple, nous sommes un fournisseur de paiement central, travaillant avec de nombreux fournisseurs de paiement du monde entier. Cependant, lorsqu'une demande est faite, je n'ai aucune idée du processeur de paiement que je vais appeler. Je pourrais programmer une classe avec une tonne de cas de commutation, tels que:
class PaymentProcessor{
private String type;
public PaymentProcessor(String type){
this.type = type;
}
public void authorize(){
if (type.equals(Consts.Paypal)){
// Do this;
}
else if(type.equals(Consts.OTHER_PROCESSOR)){
// Do that;
}
}
}
Imaginez maintenant que vous devez maintenant conserver tout ce code dans une seule classe, car il n'est pas découplé correctement. Vous pouvez imaginer que pour chaque nouveau processeur pris en charge, vous devrez créer un nouveau boîtier if // switch. Cependant, chaque méthode devient plus compliquée en utilisant Dependency Injection (ou Inversion of Control - comme on l’appelle parfois), ce qui signifie que celui qui contrôle l’exécution du programme n’est connu qu’au moment de l’exécution, sans complication), très soigné et maintenable.
class PaypalProcessor implements PaymentProcessor{
public void authorize(){
// Do Paypal authorization
}
}
class OtherProcessor implements PaymentProcessor{
public void authorize(){
// Do other processor authorization
}
}
class PaymentFactory{
public static PaymentProcessor create(String type){
switch(type){
case Consts.Paypal;
return new PaypalProcessor();
case Consts.OTHER_PROCESSOR;
return new OtherProcessor();
}
}
}
interface PaymentProcessor{
void authorize();
}
** Le code ne compilera pas, je sais :)
La principale raison d'utiliser DI est que vous souhaitez mettre la responsabilité de la connaissance de l'implémentation là où se trouve la connaissance. L'idée de DI est très en ligne avec l'encapsulation et la conception par interface. Si le serveur frontal demande des données au serveur final, le fait que le serveur final résolve la question n'a donc aucune importance. Cela dépend du gestionnaire de demandes.
Cela est déjà courant dans OOP depuis longtemps. Plusieurs fois, créant des morceaux de code comme:
I_Dosomething x = new Impl_Dosomething();
L'inconvénient est que la classe d'implémentation est toujours codée en dur, d'où la connaissance initiale de l'implémentation utilisée. DI va encore plus loin dans la conception par l’interface: la seule chose que le frontal doit savoir, c’est la connaissance de l’interface. Entre les identificateurs DYI et DI se trouve le modèle d'un localisateur de service, car le serveur frontal doit fournir une clé (présente dans le registre du localisateur de services) pour permettre à sa demande d'être résolue. Exemple de localisateur de service:
I_Dosomething x = ServiceLocator.returnDoing(String pKey);
Exemple de DI:
I_Dosomething x = DIContainer.returnThat();
L'une des exigences de DI est que le conteneur doit pouvoir déterminer quelle classe est l'implémentation de quelle interface. Par conséquent, un conteneur DI nécessite une conception fortement typée et une seule implémentation pour chaque interface à la fois. Si vous avez besoin de plusieurs implémentations d'une interface en même temps (comme une calculatrice), vous avez besoin du localisateur de service ou du modèle de conception d'usine.
D (b) I: Injection de dépendances et conception par interface. Cette restriction n’est cependant pas un très gros problème pratique. L’utilisation de D (b) I présente l’avantage de servir la communication entre le client et le fournisseur. Une interface est une perspective sur un objet ou un ensemble de comportements. Ce dernier est crucial ici.
Je préfère l’administration des contrats de service avec D (b) I en codage. Ils devraient aller ensemble. L’utilisation de D (b) I en tant que solution technique sans administration organisationnelle de contrats de service n’est pas très bénéfique à mon avis, car l’ID n’est alors qu’une couche supplémentaire d’encapsulation. Mais lorsque vous pouvez l'utiliser conjointement avec l'administration organisationnelle, vous pouvez vraiment utiliser le principe d'organisation proposé par D (b) I. Il peut vous aider à long terme à structurer la communication avec le client et les autres services techniques sur des sujets tels que les tests, la gestion des versions et le développement d’alternatives. Lorsque vous avez une interface implicite, comme dans une classe codée en dur, est-elle beaucoup moins communicable dans le temps que lorsque vous la rendez explicite à l'aide de D (b) I. Tout se résume à la maintenance, qui est au fil du temps et non à la fois. :-)