Inversion du contrôle est le concept qui permet à un framework de faire appel au code de l'utilisateur. Il s'agit d'un concept très abstrait, mais qui décrit essentiellement la différence entre une bibliothèque et un framework. L'IoC peut être considéré comme la "caractéristique déterminante d'un framework". En tant que développeurs de programmes, nous faisons appel à des bibliothèques, mais les frameworks font plutôt appel à notre code ; le framework a le contrôle, c'est pourquoi nous disons que le contrôle est inversé. Tout framework fournit des crochets qui nous permettent de brancher notre code.
L'inversion de contrôle est un modèle qui ne peut être appliqué que par les développeurs de frameworks, ou peut-être lorsque vous êtes un développeur d'applications interagissant avec le code du framework. L'IoC ne s'applique cependant pas lorsqu'on travaille exclusivement avec du code d'application.
L'acte de dépendre des abstractions au lieu des implémentations est appelé Inversion de dépendance et l'inversion de dépendances peuvent être pratiqués par les développeurs d'applications et de cadres. Ce que vous appelez IoC est en fait l'inversion de dépendance, et comme Krzysztof l'a déjà dit : ce que vous faites n'est pas IoC. Je parlerai de l'inversion de dépendance dans le reste de ma réponse.
Il existe essentiellement deux formes d'inversion de dépendance :
- Localisateur de services
-
Injection de dépendances .
Commençons par le Modèle de localisateur de services .
Le modèle de localisateur de services
Un Service Locator fournit aux composants d'application situés en dehors du [chemin de démarrage de votre application] un accès à un ensemble illimité de dépendances. Dans sa forme la plus implémentée, le Service Locator est une usine statique qui peut être configurée avec des services concrets avant que le premier consommateur ne commence à l'utiliser. (Mais vous trouverez également des Service Locator abstraits). [ source ]
Voici un exemple de localisateur de services statique :
public class Service
{
public void SomeOperation()
{
IDependency dependency =
ServiceLocator.GetInstance<IDependency>();
dependency.Execute();
}
}
Cet exemple devrait vous sembler familier, parce que c'est ce que vous faites dans votre Logon
méthode : Vous utilisez le modèle Service Locator.
Nous disons qu'un Localisateur de Services fournit l'accès à une ensemble non limité de dépendances car l'appelant peut passer le type qu'il souhaite au moment de l'exécution. Ceci est opposé à l'approche Modèle d'injection de dépendances .
Le modèle d'injection de dépendances
Avec le modèle d'injection de dépendances (DI), on peut en déclarant statiquement les dépendances requises d'une classe, généralement en les définissant dans le constructeur. Les dépendances font partie de la signature de la classe. La classe elle-même n'est pas responsable de l'obtention de ses dépendances ; cette responsabilité est déplacée vers le haut de la pile d'appels. Lors du remaniement de l'ancienne version Service
avec DI, il deviendrait probablement le suivant :
public class Service
{
private readonly IDependency dependency;
public Service(IDependency dependency)
{
this.dependency = dependency;
}
public void SomeOperation()
{
this.dependency.Execute();
}
}
Comparaison des deux modèles
Les deux modèles sont des dépendances Inversion puisque, dans les deux cas, le Service
n'est pas responsable de la création des dépendances et ne sait pas quelle implémentation elle utilise. Elle parle simplement à une abstraction. Ces deux modèles vous donnent de la flexibilité sur les implémentations qu'une classe utilise et vous permettent donc d'écrire des logiciels plus flexibles.
Le modèle Service Locator présente toutefois de nombreux problèmes, et c'est pourquoi il est considéré comme un anti-modèle . Vous êtes déjà confronté à ces problèmes, car vous vous demandez comment Service Locator, dans votre cas, vous aide à effectuer des tests unitaires.
La réponse est que le modèle Service Locator ne facilite pas les tests unitaires. Au contraire : il rend les tests unitaires plus difficiles par rapport au DI. En laissant la classe appeler le ObjectFactory
(qui est votre Service Locator), vous créez une dépendance dure entre les deux. Remplacer IAccountRepository
pour les tests, signifie également que votre test unitaire doit utiliser l'option ObjectFactory
. Cela rend vos tests unitaires plus difficiles à lire. Mais plus important encore, étant donné que le ObjectFactory
est une instance statique, tous les tests unitaires utilisent cette même instance, ce qui rend difficile l'exécution des tests de manière isolée et le remplacement des implémentations pour chaque test.
J'avais l'habitude d'utiliser un modèle de Service Locator statique dans le passé, et la façon dont je gérais cela était d'enregistrer les dépendances dans un Service Locator que je pouvais modifier sur une base thread-par-thread (en utilisant [ThreadStatic]
sous les couvertures). Cela m'a permis d'exécuter mes tests en parallèle (ce que MSTest fait par défaut) tout en gardant les tests isolés. Le problème avec cette méthode, malheureusement, c'est qu'elle est devenue très vite compliquée, qu'elle a encombré les tests de toutes sortes de trucs techniques, et qu'elle m'a fait passer beaucoup de temps à résoudre ces problèmes techniques, alors que j'aurais pu écrire plus de tests à la place.
Mais même si vous utilisez une solution hybride où vous injectez une abstraite IObjectFactory
(un Service Locator abstrait) dans le constructeur de Logon
les tests sont toujours plus difficiles que ceux de l'ID, en raison de la relation implicite entre l'AD et l'AD. Logon
et ses dépendances ; un test ne peut pas voir immédiatement quelles dépendances sont nécessaires. De plus, en plus de fournir les dépendances nécessaires, chaque test doit maintenant fournir un fichier ObjectFactory
à la classe.
Conclusion
La véritable solution aux problèmes que pose le Service Locator est DI. Une fois que vous déclarez statiquement les dépendances d'une classe dans le constructeur et que vous les injectez depuis l'extérieur, tous ces problèmes disparaissent. Non seulement cela rend très clair les dépendances dont une classe a besoin (pas de dépendances cachées), mais chaque test unitaire est lui-même responsable de l'injection des dépendances dont il a besoin. Cela facilite grandement l'écriture des tests et vous évite d'avoir à configurer un DI Container dans vos tests unitaires.