4 votes

Comment l'inversion du contrôle peut-elle m'aider ?

J'essaie de comprendre l'inversion de contrôle et comment elle m'aide dans mes tests unitaires. J'ai lu plusieurs explications en ligne sur l'IOC et ce qu'elle fait, mais je n'arrive pas à la comprendre.

J'ai développé un projet d'exemple, qui incluait l'utilisation de StructureMap pour les tests unitaires. StructureMap configure le code comme suit :

private readonly IAccountRepository _accountRepository

public Logon()
{
    _accountRepository = ObjectFactory.GetInstance<IAccountRepository>();
}

Ce que je ne comprends pas, c'est que, selon moi, je pourrais simplement déclarer ce qui précède comme suit :

AccountRepository _accountRepository = new AccountRepository();

Et il ferait la même chose que le code précédent. Je me demandais donc si quelqu'un pouvait m'expliquer de manière simple l'avantage d'utiliser l'IOC (surtout lorsqu'il s'agit de tests unitaires).

Merci

2voto

Steven Points 56939

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.

1voto

TGH Points 15623

L'idée est de vous permettre de remplacer l'implémentation par défaut du dépôt de comptes par une version plus facile à tester. Dans vos tests unitaires, vous pouvez maintenant instancier une version qui ne fait pas d'appel à la base de données, mais qui renvoie des données fixes. Ainsi, vous pouvez vous concentrer sur le test de la logique de vos méthodes et vous libérer de la dépendance à la base de données.

C'est mieux à plusieurs niveaux : 1) Vos tests sont plus stables car vous n'avez plus à vous soucier de l'échec des tests dû aux changements de données dans la base de données. 2) Vos tests s'exécuteront plus rapidement puisque vous n'avez pas à faire appel à une source de données externe. 3) Vous pouvez plus facilement simuler toutes les conditions de vos tests puisque votre référentiel fantaisie peut renvoyer tout type de données nécessaires pour tester n'importe quelle condition.

0voto

Sergio Romero Points 1525

La clé pour répondre à votre question est la testabilité et si vous voulez gérer la durée de vie des objets injectés ou si vous allez laisser le conteneur IoC le faire pour vous.

Disons par exemple que vous écrivez une classe qui utilise votre référentiel et que vous voulez la tester.

Si vous faites quelque chose comme ce qui suit :

public class MyClass
{
   public MyEntity GetEntityBy(long id)
   {
      AccountRepository _accountRepository = new AccountRepository();

      return _accountRepository.GetEntityFromDatabaseBy(id);
   }
}

Lorsque vous essayez de tester cette méthode, vous vous apercevez qu'il y a beaucoup de complications : 1. Il doit y avoir une base de données déjà configurée. 2. Votre base de données doit avoir la table qui contient l'entité que vous recherchez. 3. L'identifiant que vous utilisez pour votre test doit exister, si vous le supprimez pour quelque raison que ce soit, votre test automatisé est maintenant interrompu.

Si, au lieu de cela, vous avez quelque chose comme ce qui suit :

public interface IAccountRepository
{
   AccountEntity GetAccountFromDatabase(long id);
}

public class AccountRepository : IAccountRepository
{
   public AccountEntity GetAccountFromDatabase(long id)
   {
      //... some DB implementation here
   }
}

public class MyClass
{
   private readonly IAccountRepository _accountRepository;

   public MyClass(IAccountRepository accountRepository)
   {
      _accountRepository = accountRepository;
   }

   public AccountEntity GetAccountEntityBy(long id)
   {
      return _accountRepository.GetAccountFromDatabase(id)
   }
}

Maintenant que vous l'avez, vous pouvez tester la classe MyClass de manière isolée sans avoir besoin d'une base de données en place.

En quoi cela est-il bénéfique ? Par exemple, vous pourriez faire quelque chose comme ceci (en supposant que vous utilisez Visual Studio, mais les mêmes principes s'appliquent à NUnit par exemple) :

[TestClass]
public class MyClassTests
{
   [TestMethod]
   public void ShouldCallAccountRepositoryToGetAccount()
   {
      FakeRepository fakeRepository = new FakeRepository();

      MyClass myClass = new MyClass(fakeRepository);

      long anyId = 1234;

      Account account = myClass.GetAccountEntityBy(anyId);

      Assert.IsTrue(fakeRepository.GetAccountFromDatabaseWasCalled);
      Assert.IsNotNull(account);
   }
}

public class FakeRepository : IAccountRepository
{
   public bool GetAccountFromDatabaseWasCalled { get; private set; }

   public Account GetAccountFromDatabase(long id)
   {
      GetAccountFromDatabaseWasCalled = true;

      return new Account();
   }
}

Ainsi, comme vous pouvez le voir, vous êtes en mesure, en toute confiance, de tester que la classe MyClass utilise une instance de IAccountRepository pour obtenir une entité Account à partir d'une base de données sans avoir besoin d'avoir une base de données en place.

Il y a un million de choses que vous pouvez encore faire ici pour améliorer l'exemple. Vous pourriez utiliser un framework de Mocking comme Rhino Mocks ou Moq pour créer vos faux objets au lieu de les coder vous-même comme je l'ai fait dans l'exemple.

En faisant cela, la classe MyClass est complètement indépendante de l'AccountRepository. C'est à ce moment que le concept de couplage souple entre en jeu et que votre application est testable et plus facile à maintenir.

Avec cet exemple, vous pouvez voir les avantages de l'IoC en soi. Maintenant, si vous n'utilisez PAS de conteneur IoC, vous devez instancier toutes les dépendances et les injecter de manière appropriée dans un fichier Composition Racine ou configurer un conteneur IoC pour qu'il le fasse à votre place.

Regards.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X