.NET Framework Pourquoi nous utilisons des conteneurs d'injection de dépendance (conteneurs IoC)


Exemple

L'injection de dépendances consiste à écrire des classes pour qu'elles ne contrôlent pas leurs dépendances - au lieu de cela, leurs dépendances leur sont fournies ("injectées").

Ce n'est pas la même chose que d'utiliser un framework d'injection de dépendances (souvent appelé "conteneur DI", "conteneur IoC" ou simplement "conteneur") comme Castle Windsor, Autofac, SimpleInjector, Ninject, Unity ou autres.

Un conteneur facilite l'injection de dépendance. Par exemple, supposons que vous écrivez un certain nombre de classes qui reposent sur l'injection de dépendances. Une classe dépend de plusieurs interfaces, les classes qui implémentent ces interfaces dépendent d'autres interfaces, etc. Certains dépendent de valeurs spécifiques. Et juste pour le plaisir, certaines de ces classes implémentent IDisposable et doivent être éliminées.

Chaque cours individuel est bien écrit et facile à tester. Mais maintenant, il y a un problème différent: la création d'une instance d'une classe est devenue beaucoup plus compliquée. Supposons que nous créons une instance d'une classe CustomerService . Il a des dépendances et ses dépendances ont des dépendances. Construire une instance peut ressembler à ceci:

public CustomerData GetCustomerData(string customerNumber)
{
    var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"];
    var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"];
    var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString;
    using(var logWriter = new LogWriter(logFilePath ))
    {
        using(var customerApiClient = new CustomerApiClient(customerApiEndpoint))
        {
            var customerService = new CustomerService(
                new SqlAuthorizationRepository(authorizationConnectionString, logWriter),
                new CustomerDataRepository(customerApiClient, logWriter),
                logWriter
            );   
            
            // All this just to create an instance of CustomerService!         
            return customerService.GetCustomerData(string customerNumber);
        }
    }
}

Vous pourriez vous demander, pourquoi ne pas mettre toute la construction géante dans une fonction distincte qui renvoie simplement CustomerService ? Une des raisons est que, parce que les dépendances de chaque classe y sont injectées, une classe n'est pas responsable de savoir si ces dépendances sont IDisposable ou de les éliminer. Il les utilise juste. Donc , si nous avions un un GetCustomerService() fonction qui retourne un entièrement construit CustomerService , cette classe peut contenir un certain nombre de ressources disponibles et aucun moyen d'accéder ou de les disposer.

Et mis à part disposer de IDisposable , qui veut appeler une série de constructeurs imbriqués comme ça, jamais? C'est un court exemple. Cela pourrait être bien pire. Encore une fois, cela ne signifie pas que nous avons écrit les classes dans le mauvais sens. Les classes peuvent être individuellement parfaites. Le défi consiste à les composer ensemble.

Un conteneur d'injection de dépendance simplifie cela. Cela nous permet de spécifier quelle classe ou valeur doit être utilisée pour remplir chaque dépendance. Cet exemple légèrement simplifié utilise Castle Windsor:

var container = new WindsorContainer()
container.Register(
    Component.For<CustomerService>(),
    Component.For<ILogWriter, LogWriter>()
        .DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")),
    Component.For<IAuthorizationRepository, SqlAuthorizationRepository>()
        .DependsOn(Dependency.OnValue(connectionString, ConfigurationManager.ConnectionStrings["authorization"].ConnectionString)),
    Component.For<ICustomerDataProvider, CustomerApiClient>()
         .DependsOn(Dependency.OnAppSettingsValue("apiEndpoint", "customerApi:customerApiEndpoint"))   
);

Nous appelons cela "enregistrer des dépendances" ou "configurer le conteneur". Traduit, cela dit à notre WindsorContainer :

  • Si une classe requiert ILogWriter , créez une instance de LogWriter . LogWriter nécessite un chemin de fichier. Utilisez cette valeur depuis AppSettings .
  • Si une classe requiert IAuthorizationRepository , créez une instance de SqlAuthorizationRepository . Il nécessite une chaîne de connexion. Utilisez cette valeur de la section ConnectionStrings .
  • Si une classe requiert ICustomerDataProvider , créez un CustomerApiClient et fournissez la chaîne AppSettings partir de AppSettings .

Lorsque nous demandons une dépendance à partir du conteneur, nous appelons cela "résoudre" une dépendance. C'est une mauvaise pratique de le faire directement en utilisant le conteneur, mais c'est une autre histoire. À des fins de démonstration, nous pouvons maintenant faire ceci:

var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);

Le conteneur sait que CustomerService dépend de IAuthorizationRepository et ICustomerDataProvider . Il sait quelles classes il doit créer pour répondre à ces exigences. Ces classes, à leur tour, ont plus de dépendances et le conteneur sait comment les remplir. Il créera toutes les classes nécessaires pour pouvoir renvoyer une instance de CustomerService .

Si une classe nécessite une dépendance que nous n'avons pas enregistrée, comme IDoesSomethingElse , alors, lorsque nous essayons de résoudre CustomerService cela IDoesSomethingElse une exception claire indiquant que nous n'avons rien enregistré pour répondre à cette exigence.

Chaque structure DI se comporte un peu différemment, mais en général, elles nous permettent de contrôler la manière dont certaines classes sont instanciées. Par exemple, voulons-nous créer une instance de LogWriter et la fournir à chaque classe qui dépend de ILogWriter , ou voulons-nous en créer une à chaque fois? La plupart des conteneurs ont un moyen de spécifier cela.

Qu'en est-il des classes qui implémentent IDisposable ? C'est pourquoi nous appelons container.Release(customerService); à la fin. La plupart des conteneurs (y compris Windsor) reculeront dans toutes les dépendances créées et Dispose celles qui doivent être éliminées. Si CustomerService est IDisposable il en disposera également.

L'enregistrement des dépendances, comme vu ci-dessus, peut sembler plus simple à écrire. Mais lorsque nous avons beaucoup de classes avec beaucoup de dépendances, cela porte ses fruits. Et si nous devions écrire ces mêmes classes sans utiliser l'injection de dépendance, cette même application avec beaucoup de classes deviendrait difficile à maintenir et à tester.

Cela égratigne la raison pour laquelle nous utilisons des conteneurs d'injection de dépendance. La façon dont nous configurons notre application pour en utiliser une (et l'utiliser correctement) n'est pas un sujet: il s'agit d'un certain nombre de sujets, car les instructions et les exemples varient d'un conteneur à l'autre.