Qu'est-ce que l'injection de dépendance (DI)?
Comme d'autres l'ont dit, l' injection de dépendance (DI) supprime la responsabilité de la création directe et de la gestion de la durée de vie d'autres instances d'objet dont dépend notre classe d'intérêt (classe de consommateurs) (au sens UML ). Ces instances sont plutôt transmises à notre classe de consommateur, généralement en tant que paramètres de constructeur ou via des paramètres de propriété (la gestion de l'instanciation de l'objet de dépendance et son passage à la classe de consommateur est généralement effectuée par un conteneur IoC (Inversion of Control) , mais c'est un autre sujet) .
DI, DIP et SOLID
Plus précisément, dans le paradigme des principes SOLID de conception orientée objet de Robert C Martin , DI
est l'une des implémentations possibles du principe d'inversion de dépendance (DIP) . Le DIP est D
du SOLID
mantra - autres implémentations DIP comprennent le service de localisation, et les modèles Plugin.
L'objectif du DIP est de découpler les dépendances étroites et concrètes entre les classes, et au lieu de cela, de desserrer le couplage au moyen d'une abstraction, qui peut être réalisée via un interface
, abstract class
ou pure virtual class
, selon le langage et l'approche utilisés.
Sans le DIP, notre code (j'ai appelé cette «classe consommatrice») est directement couplé à une dépendance concrète et est également souvent chargé de savoir comment obtenir et gérer une instance de cette dépendance, c'est-à-dire conceptuellement:
"I need to create/use a Foo and invoke method `GetBar()`"
Alors qu'après l'application du DIP, l'exigence est assouplie et le souci d'obtenir et de gérer la durée de vie de la Foo
dépendance a été supprimé:
"I need to invoke something which offers `GetBar()`"
Pourquoi utiliser DIP (et DI)?
Le découplage des dépendances entre classes de cette manière permet de remplacer facilement ces classes de dépendance par d'autres implémentations qui remplissent également les conditions préalables de l'abstraction (par exemple, la dépendance peut être commutée avec une autre implémentation de la même interface). De plus, comme d'autres l'ont mentionné, la raison la plus courante pour découpler des classes via le DIP est de permettre à une classe consommatrice d'être testée isolément, car ces mêmes dépendances peuvent désormais être tronquées et / ou simulées.
Une des conséquences de DI est que la gestion de la durée de vie des instances d'objets de dépendance n'est plus contrôlée par une classe consommatrice, car l'objet de dépendance est maintenant passé dans la classe consommatrice (via l'injection de constructeur ou de setter).
Cela peut être vu de différentes manières:
- Si le contrôle de la durée de vie des dépendances par la classe consommatrice doit être conservé, le contrôle peut être rétabli en injectant une fabrique (abstraite) pour créer les instances de classe de dépendance, dans la classe consommateur. Le consommateur pourra obtenir des instances via un
Create
sur l'usine selon les besoins, et disposer de ces instances une fois terminées.
- Ou, le contrôle de la durée de vie des instances de dépendance peut être abandonné à un conteneur IoC (plus d'informations à ce sujet ci-dessous).
Quand utiliser DI?
- Lorsqu'il sera probablement nécessaire de substituer une dépendance à une implémentation équivalente,
- Chaque fois que vous aurez besoin de tester les méthodes d'une classe isolément de ses dépendances,
- Là où l'incertitude de la durée de vie d'une dépendance peut justifier une expérimentation (par exemple, Hey,
MyDepClass
est thread safe - que faire si nous en faisons un singleton et injectons la même instance à tous les consommateurs?)
Exemple
Voici une implémentation C # simple. Étant donné la classe de consommation ci-dessous:
public class MyLogger
{
public void LogRecord(string somethingToLog)
{
Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
}
}
Bien qu'apparemment inoffensif, il a deux static
dépendances sur deux autres classes System.DateTime
et System.Console
, ce qui limite non seulement les options de sortie de journalisation (la journalisation sur la console ne vaudra rien si personne ne regarde), mais pire, il est difficile de tester automatiquement étant donné la dépendance à une horloge système non déterministe.
On peut cependant s’appliquer DIP
à cette classe, en faisant abstraction du souci de l’horodatage comme dépendance, et en MyLogger
ne le couplant qu’à une interface simple:
public interface IClock
{
DateTime Now { get; }
}
Nous pouvons également relâcher la dépendance à l'égard Console
d'une abstraction, telle que a TextWriter
. L'injection de dépendance est généralement implémentée sous forme d' constructor
injection (passage d'une abstraction à une dépendance en tant que paramètre au constructeur d'une classe consommatrice) ou Setter Injection
(passage de la dépendance via un setXyz()
setter ou une propriété .Net avec {set;}
défini). L'injection de constructeur est préférable, car cela garantit que la classe sera dans un état correct après la construction et permet aux champs de dépendance internes d'être marqués comme readonly
(C #) ou final
(Java). Donc, en utilisant l'injection de constructeur dans l'exemple ci-dessus, cela nous laisse:
public class MyLogger : ILogger // Others will depend on our logger.
{
private readonly TextWriter _output;
private readonly IClock _clock;
// Dependencies are injected through the constructor
public MyLogger(TextWriter stream, IClock clock)
{
_output = stream;
_clock = clock;
}
public void LogRecord(string somethingToLog)
{
// We can now use our dependencies through the abstraction
// and without knowledge of the lifespans of the dependencies
_output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
}
}
(Un béton Clock
doit être fourni, ce qui pourrait bien sûr revenir DateTime.Now
, et les deux dépendances doivent être fournies par un conteneur IoC via l'injection de constructeur)
Un test unitaire automatisé peut être construit, ce qui prouve définitivement que notre enregistreur fonctionne correctement, car nous avons maintenant le contrôle sur les dépendances - le temps, et nous pouvons espionner la sortie écrite:
[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
// Arrange
var mockClock = new Mock<IClock>();
mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
var fakeConsole = new StringWriter();
// Act
new MyLogger(fakeConsole, mockClock.Object)
.LogRecord("Foo");
// Assert
Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}
Prochaines étapes
L'injection de dépendances est invariablement associée à un conteneur d'inversion de contrôle (IoC) , pour injecter (fournir) les instances de dépendance concrètes et pour gérer les instances de durée de vie. Pendant le processus de configuration / amorçage, les IoC
conteneurs permettent de définir les éléments suivants:
- mappage entre chaque abstraction et l'implémentation concrète configurée (par exemple "chaque fois qu'un consommateur en fait la demande
IBar
, retourne une ConcreteBar
instance" )
- des stratégies peuvent être définies pour la gestion de la durée de vie de chaque dépendance, par exemple pour créer un nouvel objet pour chaque instance de consommateur, pour partager une instance de dépendance singleton entre tous les consommateurs, pour partager la même instance de dépendance uniquement sur le même thread, etc.
- Dans .Net, les conteneurs IoC connaissent les protocoles tels que
IDisposable
et prendront la responsabilité des Disposing
dépendances conformément à la gestion de la durée de vie configurée.
En règle générale, une fois les conteneurs IoC configurés / démarrés, ils fonctionnent de manière transparente en arrière-plan, ce qui permet au codeur de se concentrer sur le code à la main plutôt que de se soucier des dépendances.
La clé du code convivial DI est d'éviter le couplage statique des classes et de ne pas utiliser new () pour la création de dépendances
Comme dans l'exemple ci-dessus, le découplage des dépendances nécessite un certain effort de conception, et pour le développeur, il y a un changement de paradigme nécessaire pour briser l'habitude d' new
ingérer des dépendances directement et de faire plutôt confiance au conteneur pour gérer les dépendances.
Mais les avantages sont nombreux, en particulier dans la possibilité de tester minutieusement votre classe d'intérêt.
Remarque : La création / mappage / projection (via new ..()
) de POCO / POJO / DTO de sérialisation / Graphes d'entité / Projections JSON anonymes et autres - c'est-à-dire les classes ou enregistrements "Données uniquement" - utilisés ou renvoyés par les méthodes ne sont pas considérés comme des dépendances (dans le Sens UML) et non soumis à DI. Les new
projeter est très bien.