Injection de dépendance
Introduction
Vous vous êtes déjà fait taper sur les doigts par votre instructeur de Java préféré lorsque vous avez déclaré une liste d’éléments de la façon suivante:
public class MyClass {
private ArrayList<String> tags;
}
Vous n’avez peut-être pas compris, mais il avait raison.
Votre code ne doit pas dépendre d’une classe concrète lorsqu’il n’a besoin que des méthodes déclarés par une interface, comme c’est le cas ici.
Vous avez donc modifié votre code, pour essayer de ne dépendre que de l’interface List
:
public class MyClass {
private List<String> tags;
}
En effet, dans ce cas, modifier le type déclaré des champs, paramètres, variables, etc. est simple et réduit le couplage:
ces propriétés ne dépendront que de l’interface List
et non de ses différentes implémentation.
Cependant, lors de l’initialisation d’un champ ou d’une variable, vous ne pouvez faire autrement qu’utiliser la classe concrète:
public MyClass() {
tags = new ArrayList<String>();
}
Cette unique utilisation de ArrayList
empêche la classe MyClass
d’être complètement indépendante des classes concrètes: elle continue de
dépendre de l’interface List
et de son implémentation.
Une solution simple à ce problème, c’est d’instancier la liste d’éléments à l’extérieur de la classe et de la passer comme paramètre du constructeur:
public class MyClass {
private List<String> tags;
public MyClass(List l) {
this.tags = l;
}
}
Si cette solution résout partiellement le problème et peut améliorer la testabilité de la classe: les cas de test pourront la configurer de l’extérieur et ensuite contrôler et observer son comportement, grâce aux doublures de test.
Mais la solution rend l’utilisation de la classe plus complexe: tout client souhaitant l’utiliser devra d’abord instancier une liste et la passer comme argument. Et si plusieurs champs doivent être instanciés à l’extérieur de la classe, le code deviendra trop complexe.
Une autre solution similaire et tout aussi partielle, consiste à utiliser une méthode pour initialiser la liste d’éléments:
public class MyClass {
private List<String> tags;
public void setTags(List l) {
this.tags = l;
}
}
Cette solution non seulement présente les mêmes inconvénients que la précédente, comme aussi en introduit un nouveau: le client doit être au courant que cette méthode doit être appelée avant l’utilisation effective d’une instance de cette classe.
Le pattern «Factory» à la rescousse
Une solution intéressante pour simplifier l’initialisation de la classe MyClass
est apportée par les patrons de conception.
En effet, le patron Factory permet de simplifier la création d’objets dont la configuration est complexe.
L’idée de ce pattern est d’utiliser une autre classe ayant pour seul objectif la création d’objects dont elle est responsable
pour les fournir prêt-à-servir; la classe concrète utilisée n’étant pas connue par l’appelant.
Une implémentation possible de la solution est listée ci-dessous:
public class MyClassFactory {
public MyClass createMyClass() {
return new MyClass(new ArrayList());
}
}
Ici, la fabrique se charge de configurer la classe MyClass
lors de sa création.
MyClass
devient enfin complètement indépendante de l’implémentation de la classe List
.
Une autre solution pour simplifier l’utilisation de MyClass consiste à utiliser l’injection de dépendance, qui nous permettra de nous passer des fabriques, tout en gardant les avantages.
Injection de dépendance
L’injection de dépendance consiste à faire appel à un mécanisme externe à la classe, qui va se charger d’initialiser ses attributs, ou en d’autres termes, va se charger d’injecter automatiquement des dépendances vers des classes concrètes. Il existe plusieurs mécanismes d’injection de dépendance. Un de plus connus et aussi le plus utilisé est Spring, mais d’autres mécanismes plus légers et récents sont aussi disponibles: Google Guice, Dagger, PicoContainer ou Weld. Dans nos exemples, nous utiliserons Spring.
Spécification du point d’injection
Dans notre exemple, le rôle de l’injection de dépendance sera d’initialiser automatiquement l’attribut tags
.
Cependant, il n’y a pas de magie: on devra spécifier quels attributs doivent être initialisés et comment.
Pour ce faire, on utilisera deux spécificités du langage Java: les annotations et la réflexion.
Tout d’abord, nous allons utiliser une des annotations définies par la JSR-330,
@Inject
pour spécifier que l’attribut tags
doit être initialisé automatiquement:
import javax.inject.Inject;
public class MyClass {
@Inject
private List<String> tags;
}
Configuration
Ensuite, la réflexion sera utilisée par une Configuration (dans le jargon Spring)
ou un Module (dans le jargon Guice)
pour trouver les attributs qui seront initialisés automatiquement, ainsi que leur types, pour ensuite les initialiser.
Dans le cas de Spring, la configuration devra spécifier une méthode compatible avec le type de l’attribut tags
, List<String>
.
Voici un exemple de configuration Spring:
@Configuration
public class MyConfiguration {
@Bean
public MyClass myClass() {
return new MyClass();
}
@Bean
public List<String> produceList() {
return new LinkedList<String>();
}
}
Dans cet exemple, le type de retour de la méthode produceList()
fait de celle-ci la seule candidate pour initialiser la variable tags
.
Notez que cette configuration n’est pas spécifique à la classe MyClass
, elle peut servir à d’autres classes.
Utilisation
L’utilisation de la classe MyClass
doit passer par un Contexte, qui utilisera la configuration pour créer des instances de MyClass
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyConfiguration.class);
MyClass mine = (MyClass) ctx.getBean(MyClass.class);
}
Lorsque le contexte initialise une instance de MyClass
, grâce à la méthode getBean()
, il initialisera automatiquement l’attribut tags
grâce à la
méthode produceList()
de la classe MyConfiguration
.
Ici, le nom de la méthode n’est pas important, ce qui compte c’est le type de retour. Ainsi, pour initialiser un attribut de la classe List
,
Spring cherchera les méthodes de création (@Bean
) dont le type de retour est compatible avec List
.
On peut utiliser cette configuration également pour initialiser l’attribut avec une autre implémentation de List
et même pour ajouter quelques valeurs à cette liste.
Code source
Le code source de cet exemple est disponible sur le GitHub.
Conclusion
Grâce à l’injection de dépendance, il est possible de séparer l’initialisation des attributs de leur utilisation. Dans notre exemple, les conséquences sont les suivantes:
- La classe
MyClass
ne connaît pas la classe concrète que l’attributtags
utilise: elle ne dépend que de l’interfaceList
. - Les classes qui utilisent
MyClass
(les clientes) n’ont pas besoin de la configurer pour l’utiliser. - La liaison entre l’attribut de type
List
et la classe concrèteLinkedList
est fait par une classe tiers de configuration. - Lorsqu’on utilise Spring pour l’injection de dépendance, il est possible de remplacer la classe de configuration par un fichier XML.