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

L’injection de dépendances est un mécanisme qui permet d’implémenter le principe de l’inversion de contrôle. Il consiste à créer dynamiquement (injecter) les dépendances entre les différents objets en s’appuyant sur une description (fichier de configuration ou métadonnées) ou de manière programmatique. En programmation objet, les objets de type A dépendent d’un objet de type B si au moins une des conditions suivantes est vérifiée :
  • A possède un attribut de type B (dépendance par composition) ;
  • A est de type B (dépendance par héritage) ;
  • A dépend d’un autre objet de type C qui dépend d’un objet de type B (dépendance par transitivité) ;
  • une méthode de A appelle une méthode de B.
Si A dépend de B, cela implique que pour créer A, on a besoin de B ce qui, en pratique, n’est pas toujours le cas. Pour supprimer la dépendance, un moyen possible consiste à créer une interface I qui contiendra toutes les méthodes que A peut appeler sur B, indiquer que B implémente l’interface I, remplacer toutes les références au type B par des références à l’interface I dans A.
L’injection proprement dite peut se faire:
  • à l’instanciation : on passe l’objet b à l’instanciation de A
  • par modificateur : on passe l’objet b à une méthode de A qui va par exemple modifier un attribut (setter)
En résumé, l’injection de dépendances consiste, pour une classe, à déléguer la création de ses dépendances au code appelant qui va ensuite les injecter dans la classe correspondante. De ce fait, la création d’une instance de la dépendance est effectuée à l’extérieur de la classe dépendante et injectée dans la classe (le plus souvent dans le constructeur mais il existe d’autres possibilités).

 

Présentation de l'injection de dépendances en détail (univ-mlv.fr)

L’injection de dépendances offre plusieurs avantages significatifs :

  1. Découplage : L’injection de dépendances permet de découpler les classes et leurs dépendances. Cela signifie que les classes ne sont plus responsables de la recherche ou de la création de leurs dépendances. Cela rend le code plus modulaire et plus facile à comprendre et à maintenir.

  2. Flexibilité et réutilisabilité : L’injection de dépendances rend les objets et les applications qui les utilisent plus flexibles et réutilisables. Cela facilite la création d’objets faiblement couplés et de leurs dépendances.

  3. Testabilité : L’injection de dépendances facilite l’écriture de tests unitaires. En injectant des dépendances, vous pouvez facilement substituer des objets réels par des mocks (objets factices) pour tester votre code de manière isolée.

  4. Inversion de contrôle : L’injection de dépendance est une forme d’inversion de contrôle. Au lieu que les classes contrôlent leurs propres dépendances, elles travaillent avec des instances fournies par leur environnement extérieur.

  5. Gestion des dépendances complexes : Dans un système complexe, il est courant d’avoir un enchevêtrement de dépendances qui peut amener à des situations complexes lorsqu’il devient nécessaire de modifier une des classes. L’injection de dépendances aide à gérer ces situations en maintenant une relation saine entre les différents objets.

  6. Couplage faible : L’injection de dépendances favorise le couplage faible en permettant à une classe d’être indépendante des classes dont elle dépend1. Cela signifie que vous pouvez modifier une dépendance sans avoir à modifier les classes qui l’utilisent.

  7. Réutilisabilité : Les classes qui utilisent l’injection de dépendances sont généralement plus réutilisables car elles peuvent être utilisées dans différents contextes avec différentes implémentations de leurs dépendances

  8.  

    Maintenabilité : Si vos classes sont faiblement couplées et suivent le principe de responsabilité unique, ce qui est le résultat naturel de l’utilisation de l’injection de dépendances, alors votre code sera plus facile à maintenir2. Des classes simples et autonomes sont plus faciles à corriger que des classes compliquées et fortement couplées.

  9. Lisibilité : Le code qui utilise l’injection de dépendances est plus simple. Il suit le principe de responsabilité unique et, par conséquent, donne lieu à des classes plus petites, plus compactes et plus précises. Les constructeurs ne sont pas aussi encombrés et remplis de logique. Les classes sont plus clairement définies, déclarant ouvertement ce dont elles ont besoin.

    En somme, l’injection de dépendances peut rendre votre code plus propre, plus modulaire et plus facile à tester et à maintenir

Exemple commenté

  1. Définir les dépendances : Chaque classe qui entre en jeu dans l’injection de dépendances doit se déclarer dans le conteneur d’inversion de contrôle (IoC container). Par exemple, si vous avez des classes comme AbstractResourceManagerFactoryImplProjetManagerImplProjetDaoImpl, etc., chacune d’elles doit être déclarée dans l’IoC container.

  2. Indiquer les dépendances à injecter : Pour chaque objet, indiquez les dépendances à injecter. Par exemple, si ManagerFactoryImpl a besoin d’un ProjetManager et d’un TicketManager, vous devez indiquer ces dépendances.

  3. Utiliser un conteneur d’inversion de contrôle (IoC container) : L’IoC container se charge d’instancier et d’injecter les dépendances afin de produire la grappe d’objets pleinement configurée.

 

 

Voici un exemple de code sans injection de dépendances:

public class PizzaManagerImpl implements PizzaManager {
    private final PizzaDao pizzaDao = new PizzaDaoImpl();

    @Override
    public List<Pizza> menu() {
        return pizzaDao.getAll();
    }
}

Dans cet exemple, la classe PizzaManagerImpl crée une instance de PizzaDaoImpl directement en utilisant l’opérateur new. Cela signifie que PizzaManagerImpl est fortement couplé à PizzaDaoImpl et ne peut pas être utilisé avec une autre implémentation de PizzaDao sans modification du code.

Voici un exemple de code avec injection de dépendances:

public class PizzaManagerImpl implements PizzaManager {
    private final PizzaDao pizzaDao;

    public PizzaManagerImpl(PizzaDao pizzaDao) {
        this.pizzaDao = pizzaDao;
    }

    @Override
    public List<Pizza> menu() {
        return pizzaDao.getAll();
    }
}

Dans cet exemple, la classe PizzaManagerImpl reçoit une instance de PizzaDao en tant que paramètre de son constructeur. Cela signifie que PizzaManagerImpl n’est pas responsable de la création de l’instance de PizzaDao, mais qu’elle délègue cette responsabilité à une autre partie du code (généralement un conteneur d’injection de dépendances comme Spring). Cela permet à PizzaManagerImpl d’être utilisé avec n’importe quelle implémentation de PizzaDao, ce qui rend le code plus flexible et plus facile à tester.

L’injection de dépendances offre plusieurs avantages par rapport à l’instanciation directe des dépendances dans une classe. Elle favorise le couplage faible, améliore la testabilité, la réutilisabilité, la maintenabilité, la lisibilité et la flexibilité du code. En somme, l’injection de dépendances peut rendre votre code plus propre, plus modulaire et plus facile à tester et à maintenir.

 

Pour implémenter l’injection de dépendances avec Spring, vous pouvez suivre les étapes suivantes:

  1. Définir les dépendances : Chaque classe qui entre en jeu dans l’injection de dépendances doit se déclarer dans le conteneur d’inversion de contrôle (IoC container). Par exemple, si vous avez des classes comme AbstractResourceManagerFactoryImplProjetManagerImplProjetDaoImpl, etc., chacune d’elles doit être déclarée dans l’IoC container.

  2. Indiquer les dépendances à injecter : Pour chaque objet, indiquez les dépendances à injecter. Par exemple, si ManagerFactoryImpl a besoin d’un ProjetManager et d’un TicketManager, vous devez indiquer ces dépendances.

  3. Utiliser un conteneur d’inversion de contrôle (IoC container) : L’IoC container se charge d’instancier et d’injecter les dépendances afin de produire la grappe d’objets pleinement configurée.

Voici un exemple de code illustrant comment cela pourrait être fait:

import javax.inject.Inject;

public class ClassA {
    private final ComponentA componentA;

    @Inject
    public ClassA(ComponentA componentA) {
        this.componentA = componentA;
    }
}

Dans cet exemple, la classe ClassA a besoin de la classe ComponentA pour fonctionner correctement. Au lieu de créer une instance de ComponentA à l’intérieur de ClassA, nous injectons une instance de ComponentA dans le constructeur de ClassA. C’est ce qu’on appelle l’injection de dépendances.

 

L’injection de dépendances offre plusieurs avantages par rapport à l’instantiation classique:

  1. Modularité et indépendance : L’injection de dépendances rend le code plus modulable et les parties plus indépendantes4. Cela décharge le développeur de nombreuses tâches, offrant ainsi toute sa puissance à l’inversion de contrôle.

  2. Testabilité : Grâce à l’injection de dépendances, il devient beaucoup plus facile d’écrire des tests unitaires fiables et compartimentés2. Vous pouvez ainsi vous construire une batterie de tests unitaires vous permettant de vous prémunir notamment des régressions lors des évolutions de vos applications.

  3. Gestion des dépendances complexes : Dans un système complexe, il est courant d’avoir un enchevêtrement de dépendances qui peut amener à des situations complexes lorsqu’il devient nécessaire de modifier une des classes. L’injection de dépendances aide à gérer ces situations en maintenant une relation saine entre les différents objets2.

  4. Changement d’implémentation : L’injection de dépendances offre la possibilité de changer d’implémentation. Cela signifie que vous pouvez facilement remplacer une implémentation par une autre sans avoir à modifier le code qui utilise cette implémentation.

  5. Gestion des dépendances au plus haut niveau : Avec l’injection de dépendances, la gestion des dépendances se fait au plus haut niveau de l’application. Cela signifie que la création et la gestion des objets sont centralisées, ce qui facilite la maintenance et la compréhension du code.

Heureusement Spring simplifie la façon de coder avec des injections de dépendances.

Voici comment :

  1. Conteneur d’inversion de contrôle (IoC) : Spring utilise un “conteneur d’inversion de contrôle” pour gérer l’injection de dépendances. Chaque classe qui entre en jeu dans l’injection de dépendances se déclare dans l’IoC container. Pour chaque objet, on indique les dépendances à injecter. L’IoC container se charge d’instancier et d’injecter les dépendances afin de produire la grappe d’objets pleinement configurée.

  2. Injection par constructeur : Spring recommande l’utilisation de l’injection par constructeur pour vos beans. Cela signifie que vous créez un constructeur qui prend en paramètres toutes les dépendances requises. Si votre classe a besoin de plusieurs constructeurs, vous pouvez annoter celui que Spring doit utiliser avec @Autowired.

  3. Simplicité et flexibilité : Avec Spring, vous pouvez facilement changer d’approche pour simplifier l’injection de dépendances. De plus, Spring offre une grande flexibilité pour gérer les dépendances complexes, ce qui facilite la maintenance et la compréhension du code.

 

Exemple avec spring boot:

Tout d’abord, définissons notre interface et nos implémentations :

public interface Cetacean {
    void swim();
}

@Component
public class Basilosaurus implements Cetacean {
    @Override
    public void swim() {
        System.out.println("Basilosaurus is swimming...");
    }
}

@Component
public class Ambulocetus implements Cetacean {
    @Override
    public void swim() {
        System.out.println("Ambulocetus is swimming...");
    }
}

@Component
public class Dorudon implements Cetacean {
    @Override
    public void swim() {
        System.out.println("Dorudon is swimming...");
    }
}

@Component
public class Whale implements Cetacean {
    @Override
    public void swim() {
        System.out.println("Whale is swimming...");
    }
}

Ensuite, nous créons une classe CetaceanWatcher qui utilise l’interface Cetacean. Nous utilisons l’annotation @Autowired pour indiquer à Spring d’injecter une implémentation de Cetacean :

@Component
public class CetaceanWatcher {
    private final Cetacean cetacean;

    @Autowired
    public CetaceanWatcher(Cetacean cetacean) {
        this.cetacean = cetacean;
    }

    public void watch() {
        cetacean.swim();
    }
}
@Autowired est une annotation Spring qui est utilisée pour l’injection de dépendances. Lorsque vous utilisez @Autowired sur des champs, des constructeurs ou des méthodes, Spring essaie de satisfaire l’injection de dépendances pour vous automatiquement.
 
 

Enfin, nous pouvons utiliser la classe CetaceanWatcher dans notre application :

@SpringBootApplication
public class CetaceanApp {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(CetaceanApp.class, args);
        CetaceanWatcher watcher1 = new CetaceanWatcher(new Basilosaurus());
        watcher1.watch();
        CetaceanWatcher watcher2 = new CetaceanWatcher(new Ambulocetus());
        watcher2.watch();
    }
}


ou


@SpringBootApplication public class CetaceanApp { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(CetaceanApp.class, args); CetaceanWatcher watcher1 = context.getBean("basilosaurusWatcher", CetaceanWatcher.class); watcher1.watch(); CetaceanWatcher watcher2 = context.getBean("ambulocetusWatcher", CetaceanWatcher.class); watcher2.watch(); } @Bean public CetaceanWatcher basilosaurusWatcher() { return new CetaceanWatcher(new Basilosaurus()); } @Bean public CetaceanWatcher ambulocetusWatcher() { return new CetaceanWatcher(new Ambulocetus()); } }

Dans cet exemple, Spring décide quelle implémentation de Cetacean injecter dans CetaceanWatcher en fonction des beans disponibles dans le contexte de l’application. Si vous avez à la fois BasilosaurusAmbulocetusDorudon et Whale dans votre contexte d’application, vous devrez qualifier votre dépendance pour indiquer quelle implémentation utiliser.

 

C'est quoi des beans ?

Dans le contexte de Spring, on appelle souvent les objets gérés par le conteneur d’inversion de contrôle (IoC) de Spring des “beans”. Un bean est un objet qui est instancié, assemblé et géré par un conteneur IoC. Spring utilise l’injection de dépendances pour gérer ces beans et leurs dépendances.

Dans l’exemple précédent, BasilosaurusAmbulocetusDorudon et Whale sont tous des beans de Spring. Ils sont annotés avec @Component, ce qui indique à Spring qu’il doit gérer ces classes comme des beans. De même, CetaceanWatcher est également un bean de Spring.

Lorsque vous exécutez l’application, Spring crée des instances de ces beans, résout leurs dépendances en utilisant l’injection de dépendances, et les injecte là où elles sont nécessaires.

 

Modifié le: vendredi 8 septembre 2023, 04:28