Bases de l'injection de dépendance

Bases de l'injection de dépendance


Dans cet article, je vais parler des bases de l'injection de dépendance (Eng. Dependency Injection, DI ) dans un langage simple, ainsi que des raisons d'utiliser cette approche. Cet article est destiné à ceux qui ne savent pas ce qu'est l'injection de dépendance ou qui doutent de la nécessité d'utiliser cette technique. Commençons donc.


Qu'est-ce que la dépendance?


Regardons d'abord un exemple. Nous avons ClassA , ClassB et ClassC comme indiqué ci-dessous:


 class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { } 

Vous pouvez voir que la classe ClassA contient une instance de la classe ClassB , nous pouvons donc dire que la classe ClassA dépend de la classe ClassB . Pourquoi? Parce que ClassA besoin de ClassB pour fonctionner correctement. On peut également dire que la classe ClassB est une dépendance de la classe ClassA .


Avant de continuer, je tiens à préciser qu'une telle relation est bonne, car nous n'avons pas besoin d'une seule classe pour faire tout le travail dans l'application. Nous devons diviser la logique en différentes classes, chacune étant responsable d'une certaine fonction. Et dans ce cas, les classes pourront interagir efficacement.


Comment travailler avec des dépendances?


Examinons trois méthodes utilisées pour effectuer des tâches d'injection de dépendances:


Première façon: créer des dépendances dans une classe dépendante


Autrement dit, nous pouvons créer des objets chaque fois que nous en avons besoin. Jetez un œil à l'exemple suivant:


 class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } } 

C'est très simple! Nous créons une classe quand nous en avons besoin.


Les avantages


  • C'est facile et simple.
  • La classe dépendante ( ClassA dans notre cas) contrôle entièrement comment et quand créer les dépendances.

Inconvénients


  • ClassA et ClassB étroitement liées l'une à l'autre. Par conséquent, chaque fois que nous aurons besoin d'utiliser ClassA , nous serons obligés d'utiliser ClassB et il sera impossible de remplacer ClassB par autre chose .
  • Avec tout changement dans l'initialisation de la classe ClassB , vous devrez ajuster le code à l'intérieur de la classe ClassA (et toutes les autres classes dépendantes de ClassB ). Cela complique le processus de changement de dépendance.
  • ClassA ne peut pas être testée. Si vous avez besoin de tester une classe, et pourtant c'est l'un des aspects les plus importants du développement logiciel, vous devrez effectuer des tests unitaires de chaque classe séparément. Cela signifie que si vous souhaitez vérifier ClassA le bon fonctionnement de la classe ClassA et créer plusieurs tests unitaires pour le vérifier, alors, comme indiqué dans l'exemple, vous créerez dans tous les cas également une instance de la classe ClassB , même si cela ne vous intéresse pas. Si une erreur se produit pendant le test, vous ne pourrez pas comprendre où elle se trouve - dans ClassA ou ClassB . Après tout, il est possible qu'une partie du code de la ClassB conduit à une erreur, alors que la ClassA fonctionne correctement. En d'autres termes, le test unitaire n'est pas possible car les modules (classes) ne peuvent pas être séparés les uns des autres.
  • ClassA doit être configuré pour pouvoir injecter des dépendances. Dans notre exemple, il doit savoir comment créer un ClassC et l'utiliser pour créer un ClassB . Ce serait mieux s'il n'en savait rien. Pourquoi? En raison du principe de la responsabilité unique .

Chaque classe ne devrait faire que son travail.

Par conséquent, nous ne voulons pas que les classes soient responsables d'autre chose que de leurs propres tâches. L'implémentation des dépendances est une tâche supplémentaire que nous leur avons assignée.


Deuxième façon: injecter des dépendances via une classe personnalisée


Donc, comprendre que l'injection de dépendances dans une classe dépendante n'est pas une bonne idée, explorons une autre façon. Ici, la classe dépendante définit toutes les dépendances dont elle a besoin à l'intérieur du constructeur et permet à la classe utilisateur de les fournir. Est-ce une solution à notre problème? Nous le saurons un peu plus tard.


Jetez un œil à l'exemple de code ci-dessous:


 class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium - 

Maintenant, ClassA obtient toutes les dépendances à l'intérieur du constructeur et peut simplement appeler les méthodes de la classe ClassB sans rien initialiser.


Les avantages


  • ClassA et ClassB désormais faiblement couplés, et nous pouvons remplacer ClassB sans casser le code à l'intérieur de ClassA . Par exemple, au lieu de passer ClassB nous pouvons passer AssumeClassB , qui est une sous-classe de ClassB , et notre programme fonctionnera correctement.
  • ClassA peut désormais être testée. Lors de l'écriture d'un test unitaire, nous pouvons créer notre propre version de ClassB (objet de test) et la transmettre à ClassA . Si une erreur se produit lors de la réussite du test, nous savons maintenant avec certitude qu'il s'agit bien d'une erreur dans ClassA .
  • ClassB libre de travailler avec des dépendances et peut se concentrer sur ses tâches.

Inconvénients


  • Cette méthode ressemble à un mécanisme de chaîne, et à un moment donné, la chaîne doit être interrompue. En d'autres termes, l'utilisateur de la classe ClassA doit tout savoir sur l'initialisation de ClassB , ce qui nécessite à son tour des connaissances sur l'initialisation de ClassC , etc. Ainsi, vous voyez que tout changement dans le constructeur de l'une de ces classes peut entraîner un changement dans la classe appelante, sans oublier que ClassA peut avoir plus d'un utilisateur, de sorte que la logique de création d'objets sera répétée.
  • Malgré le fait que nos dépendances sont claires et faciles à comprendre, le code utilisateur n'est pas trivial et difficile à gérer. Par conséquent, tout n'est pas si simple. En outre, le code viole le principe de la responsabilité unique, car il est responsable non seulement de son travail, mais également de la mise en œuvre des dépendances dans les classes dépendantes.

La deuxième méthode fonctionne évidemment mieux que la première, mais elle a toujours ses défauts. Est-il possible de trouver une solution plus adaptée? Avant d'envisager la troisième voie, parlons d'abord du concept même d'injection de dépendance.


Qu'est-ce que l'injection de dépendance?


L'injection de dépendances est un moyen de gérer les dépendances en dehors de la classe dépendante lorsque la classe dépendante n'a rien à faire.

Sur la base de cette définition, notre première solution n'utilise évidemment pas l'idée d'injection de dépendances, et la deuxième manière est que la classe dépendante ne fait rien pour fournir les dépendances. Mais nous pensons toujours que la deuxième solution est mauvaise. POURQUOI?!


Étant donné que la définition de l'injection de dépendance ne dit rien sur l'endroit où le travail avec les dépendances doit avoir lieu (sauf en dehors de la classe dépendante), le développeur doit choisir un endroit approprié pour l'injection de dépendance. Comme vous pouvez le voir dans le deuxième exemple, la classe d'utilisateurs n'est pas tout à fait au bon endroit.


Comment faire mieux? Examinons une troisième façon de gérer les dépendances.


Troisième façon: laissez quelqu'un d'autre gérer les dépendances au lieu de nous


Selon la première approche, les classes dépendantes sont responsables de l'obtention de leurs propres dépendances, et dans la deuxième approche, nous avons déplacé le traitement des dépendances de la classe dépendante vers la classe utilisateur. Imaginons qu'il y ait quelqu'un d'autre qui pourrait gérer les dépendances, à la suite de quoi ni les classes dépendantes ni les classes d'utilisateurs ne feraient le travail. Cette méthode vous permet de travailler directement avec les dépendances dans l'application.


Une implémentation «propre» de l'injection de dépendance (à mon avis)

La responsabilité du traitement des dépendances incombe à un tiers, donc aucune partie de l'application n'interagira avec lui.

L'injection de dépendances n'est pas une technologie, un cadre, une bibliothèque ou quelque chose comme ça. Ce n'est qu'une idée. L'idée est de travailler avec des dépendances en dehors de la classe dépendante (de préférence dans une partie spécialement allouée). Vous pouvez appliquer cette idée sans utiliser de bibliothèques ou de frameworks. Cependant, nous nous tournons généralement vers les frameworks pour implémenter les dépendances, car cela simplifie le travail et évite d'écrire du code de modèle.


Tout cadre d'injection de dépendance a deux caractéristiques inhérentes. D'autres fonctions supplémentaires peuvent être à votre disposition, mais ces deux fonctions seront toujours présentes:


Premièrement, ces cadres offrent un moyen de déterminer les champs (objets) à mettre en œuvre. Certains frameworks le font en annotant un champ ou un constructeur à l'aide de l'annotation @Inject , mais il existe d'autres méthodes. Par exemple, Koin utilise les fonctionnalités du langage intégré de Kotlin pour déterminer la mise en œuvre. Inject signifie que la dépendance doit être gérée par le framework DI. Le code ressemblera à ceci:


 class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } } 

Deuxièmement, les frameworks vous permettent de déterminer comment fournir chaque dépendance, et cela se produit dans un ou plusieurs fichiers distincts. Cela ressemble approximativement à ceci (gardez à l'esprit qu'il ne s'agit que d'un exemple et qu'il peut différer d'un cadre à l'autre):


 class OurThirdPartyGuy { fun provideClassC(){ return ClassC() //just creating an instance of the object and return it. } fun provideClassB(classC: ClassC){ return ClassB(classC) } fun provideClassA(classB: ClassB){ return ClassA(classB) } } 

Ainsi, comme vous pouvez le voir, chaque fonction est responsable du traitement d'une dépendance. Par conséquent, si nous devons utiliser ClassA quelque part dans l'application, les ClassA suivants se produiront: notre ClassC DI crée une instance de la classe ClassC en appelant provideClassC , en la passant à provideClassB et en recevant une instance de ClassB , qui est transmise à provideClassA , et en conséquence, ClassA est créé. C'est presque magique. Examinons maintenant les avantages et les avantages de la troisième méthode.


Les avantages


  • Tout est aussi simple que possible. La classe dépendante et la classe qui fournit les dépendances sont claires et simples.
  • Les classes sont faiblement couplées et sont facilement remplaçables par d'autres classes. Supposons que nous voulons remplacer ClassC par AssumeClassC , qui est une sous-classe de ClassC . Pour ce faire, il vous suffit de modifier le code du fournisseur comme suit, et partout où ClassC est utilisé, la nouvelle version sera désormais automatiquement utilisée:

 fun provideClassC(){ return AssumeClassC() } 

Veuillez noter qu'aucun code à l'intérieur de l'application ne change, seulement la méthode du fournisseur. Il semble que rien ne pourrait être encore plus simple et plus flexible.


  • Testabilité incroyable. Vous pouvez facilement remplacer les dépendances par des versions de test pendant le test. En fait, l'injection de dépendance est votre principale aide en matière de test.
  • Améliorer la structure du code, comme l'application a une place distincte pour le traitement des dépendances. Par conséquent, le reste de l'application peut se concentrer exclusivement sur ses fonctions et ne pas chevaucher de dépendances.

Inconvénients


  • Les cadres DI ont un certain seuil d'entrée, donc l'équipe de projet doit passer du temps et l'étudier avant de l'utiliser efficacement.

Conclusion


  • La gestion des dépendances sans DI est possible, mais elle peut entraîner des pannes d'application.
  • DI n'est qu'une idée efficace, selon laquelle il est possible de gérer des dépendances en dehors de la classe dépendante.
  • Il est plus efficace d'utiliser DI dans certaines parties de l'application. De nombreux cadres y contribuent.
  • Les cadres et les bibliothèques ne sont pas nécessaires pour DI, mais ils peuvent beaucoup aider.

Dans cet article, j'ai essayé d'expliquer les bases de l'utilisation du concept d'injection de dépendance et j'ai également énuméré les raisons pour lesquelles cette idée était utilisée. Il existe de nombreuses autres ressources que vous pouvez explorer pour en savoir plus sur l'utilisation de DI dans vos propres applications. Par exemple, une section distincte dans la partie avancée de notre cours de profession Android est dédiée à ce sujet.

Source: https://habr.com/ru/post/fr434380/


All Articles