.NET Framework
Injection de dépendance
Recherche…
Remarques
Problèmes résolus par injection de dépendance
Si nous n'utilisions pas d'injection de dépendance, la classe Greeter
pourrait ressembler davantage à ceci:
public class ControlFreakGreeter
{
public void Greet()
{
var greetingProvider = new SqlGreetingProvider(
ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
var greeting = greetingProvider.GetGreeting();
Console.WriteLine(greeting);
}
}
C'est un "contrôle freak" car il contrôle la création de la classe qui fournit le message d'accueil, il contrôle la provenance de la chaîne de connexion SQL et contrôle la sortie.
En utilisant l'injection de dépendance, la classe Greeter
renonce à ces responsabilités en faveur d'une responsabilité unique, en lui écrivant un message d'accueil.
Le principe d'inversion de dépendance suggère que les classes devraient dépendre des abstractions (comme les interfaces) plutôt que d'autres classes concrètes. Les dépendances directes (couplage) entre classes peuvent rendre la maintenance progressivement difficile. Selon les abstractions peuvent réduire ce couplage.
L'injection de dépendance nous aide à réaliser cette inversion de dépendance car elle conduit à écrire des classes qui dépendent des abstractions. La classe Greeter
"sait" rien des détails d' IGreetingProvider
de IGreetingProvider
et IGreetingWriter
. Il sait seulement que les dépendances injectées implémentent ces interfaces. Cela signifie que les modifications apportées aux classes concrètes qui implémentent IGreetingProvider
et IGreetingWriter
n'affecteront pas Greeter
. Ne les remplaceront pas non plus par des implémentations entièrement différentes. Seules les modifications apportées aux interfaces le seront. Greeter
est découplé.
ControlFreakGreeter
est impossible à tester correctement. Nous voulons tester une petite unité de code, mais à la place, notre test comprendra la connexion à SQL et l'exécution d'une procédure stockée. Il faudrait également tester la sortie de la console. Parce que ControlFreakGreeter fait tellement de choses, il est impossible de tester indépendamment des autres classes.
Greeter
est facile à tester car nous pouvons injecter des implémentations simulées de ses dépendances qui sont plus faciles à exécuter et à vérifier qu’à appeler une procédure stockée ou à lire la sortie de la console. Il ne nécessite pas de chaîne de connexion dans app.config.
Les implémentations concrètes de IGreetingProvider
et IGreetingWriter
pourraient devenir plus complexes. Ils peuvent à leur tour avoir leurs propres dépendances qui leur sont injectées. (Par exemple, nous injecterions la chaîne de connexion SQL dans SqlGreetingProvider
.) Mais cette complexité est "cachée" des autres classes qui ne dépendent que des interfaces. Cela facilite la modification d'une classe sans "effet d'entraînement", ce qui nous oblige à apporter des modifications correspondantes aux autres classes.
Injection de dépendance - Exemple simple
Cette classe s'appelle Greeter
. Sa responsabilité est de générer un message d'accueil. Il a deux dépendances . Il a besoin de quelque chose qui lui donnera le message d'accueil, et il lui faudra alors un moyen de sortir ce message d'accueil. Ces dépendances sont toutes deux décrites comme des interfaces, IGreetingProvider
et IGreetingWriter
. Dans cet exemple, ces deux dépendances sont "injectées" dans Greeter
. (Plus d'explications suivant l'exemple.)
public class Greeter
{
private readonly IGreetingProvider _greetingProvider;
private readonly IGreetingWriter _greetingWriter;
public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter)
{
_greetingProvider = greetingProvider;
_greetingWriter = greetingWriter;
}
public void Greet()
{
var greeting = _greetingProvider.GetGreeting();
_greetingWriter.WriteGreeting(greeting);
}
}
public interface IGreetingProvider
{
string GetGreeting();
}
public interface IGreetingWriter
{
void WriteGreeting(string greeting);
}
La classe de IGreetingProvider
Greeting
dépend à la fois de IGreetingProvider
et de IGreetingWriter
, mais elle n'est pas responsable de la création des instances de l'un ou de l'autre. Au lieu de cela, il les requiert dans son constructeur. Tout ce qui crée une instance de Greeting
d' Greeting
doit fournir ces deux dépendances. Nous pouvons appeler cela "injecter" les dépendances.
Parce que les dépendances sont fournies à la classe dans son constructeur, cela s'appelle aussi "injection constructeur".
Quelques conventions communes:
- Le constructeur enregistre les dépendances en tant que champs
private
. Dès que la classe est instanciée, ces dépendances sont disponibles pour toutes les autres méthodes non statiques de la classe. - Les champs
private
sont enreadonly
. Une fois définies dans le constructeur, elles ne peuvent plus être modifiées. Cela indique que ces champs ne doivent pas (et ne peuvent pas) être modifiés en dehors du constructeur. Cela garantit en outre que ces dépendances seront disponibles pour toute la durée de la classe. - Les dépendances sont des interfaces. Ce n'est pas strictement nécessaire, mais cela est fréquent car cela facilite la substitution d'une implémentation de la dépendance à une autre. Il permet également de fournir une version simulée de l'interface à des fins de test unitaire.
Comment l'injection de dépendance facilite les tests unitaires
Cela se base sur l'exemple précédent de la classe Greeter
qui a deux dépendances, IGreetingProvider
et IGreetingWriter
.
L'implémentation réelle de IGreetingProvider
peut récupérer une chaîne à partir d'un appel API ou d'une base de données. L'implémentation de IGreetingWriter
peut afficher le IGreetingWriter
d'accueil dans la console. Mais comme Greeter
a ses dépendances injectées dans son constructeur, il est facile d'écrire un test unitaire qui injecte des versions simulées de ces interfaces. Dans la vraie vie, nous pourrions utiliser un framework comme Moq , mais dans ce cas, je vais écrire ces implémentations simulées.
public class TestGreetingProvider : IGreetingProvider
{
public const string TestGreeting = "Hello!";
public string GetGreeting()
{
return TestGreeting;
}
}
public class TestGreetingWriter : List<string>, IGreetingWriter
{
public void WriteGreeting(string greeting)
{
Add(greeting);
}
}
[TestClass]
public class GreeterTests
{
[TestMethod]
public void Greeter_WritesGreeting()
{
var greetingProvider = new TestGreetingProvider();
var greetingWriter = new TestGreetingWriter();
var greeter = new Greeter(greetingProvider, greetingWriter);
greeter.Greet();
Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting);
}
}
Le comportement de IGreetingProvider
et IGreetingWriter
ne concerne pas ce test. Nous voulons tester que Greeter
reçoit un message et l’écrit. La conception de Greeter
(en utilisant l'injection de dépendance) nous permet d'injecter des dépendances simulées sans pièces mobiles compliquées. Tout ce que nous testons, c'est que Greeter
interagit avec ces dépendances comme nous le souhaitons.
Pourquoi nous utilisons des conteneurs d'injection de dépendance (conteneurs IoC)
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 deLogWriter
.LogWriter
nécessite un chemin de fichier. Utilisez cette valeur depuisAppSettings
. - Si une classe requiert
IAuthorizationRepository
, créez une instance deSqlAuthorizationRepository
. Il nécessite une chaîne de connexion. Utilisez cette valeur de la sectionConnectionStrings
. - Si une classe requiert
ICustomerDataProvider
, créez unCustomerApiClient
et fournissez la chaîneAppSettings
partir deAppSettings
.
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.