On est dans une situation où un objet "de haut niveau" (le client) a besoin d'utiliser un objet "de plus bas niveau" (un service ou un composant logiciel).
On sépare l'utilisation du service de sa configuration.
On parle d'injection de dépendance lorsque ce n'est pas le client qui choisit quel service utiliser.
Sources : martinfowler.com
On sépare l'utilisation du service de sa configuration.
On parle d'injection de dépendance lorsque ce n'est pas le client qui choisit quel service utiliser.
Sans injection de dépendance
C'est le client qui décide quel service utiliser.// ********** client ************** class Client1{ private ServiceI service; public Client1() { service = new Service1(); // ICI le client choisit } public void action(){ System.out.println("Client.action() : " + service.doSomething()); } } // ********** service ************** interface ServiceI{ public String doSomething(); } class Service1 implements ServiceI{ public String doSomething(){ return this.getClass() + ".doSomething()"; } } // ********** main ************** public class Test1{ public static void main(String[] args){ Client1 client = new Client1(); client.action(); } }
Problèmes
Cette solution fonctionne, mais elle est rigide car le client est lié à une implémentation donnée du service.De plus, il n'est pas possible de tester le client indépendamment du service. Dans un vrai système, on peut avoir des dizaines de services, avec plusieurs implémentations possibles par service, il faut que le client puisse fonctionner avec n'importe quelle implémentation du service.
On aussi a parfois besoin de pouvoir déployer des clients utilisant des implémentations différentes des services utilisés.
On cherche à se retrouver dans une situation de plugin (des programmes complètement indépendants qui fonctionnent ensemble).
Deux patterns répondent à cette problématique : dependency injection et service locator.
On parle d'inversion de contrôle, car le client ne décide plus de l'implémentation des composants qu'il utilise.
Injection de dépendance
On définit habituellement 3 types d'injection :- Injection par constructeur
- Injection par setter
- Injection par interface
Injection par constructeur (type 3)
Le service à utiliser est "injecté" dans le client par le biais d'un constructeur.// ********** client ************** class Client2{ private ServiceI service; public Client2(ServiceI service) { this.service = service; // ICI injection } public void action(){ System.out.println("Client.action() : " + service.doSomething()); } } // ********** service ************** interface ServiceI{ public String doSomething(); } class Service2 implements ServiceI{ public String doSomething(){ return this.getClass() + ".doSomething()"; } } // ********** main ************** public class Test2{ public static void main(String[] args){ Client2 client = new Client2(new Service2()); client.action(); } }
Intérêts
- Le service est toujours disponible pour le client.
- On a bien rendu client et service indépendants.
-
On peut tester le service et le client indépendamment :
ServiceI service1 = new ServiceDeTest(); Client client1 = new ServiceDeTest(service1); ServiceI service2 = new ServiceRéel(); Client client2 = new ServiceDeTest(service2);
Inconvénients
-
Pas très flexible, par exemple si le client peut faire certaines choses sans avoir besoin du service.
Ce problème devient plus aigü si le client utilise plusieurs services.
Injection par setter (type 2)
Le client peut être créé sans service, et le service lui est injecté par le biais d'un setter.// ********** client ************** class Client3{ private ServiceI service; public void setService(ServiceI service){ this.service = service; // ICI injection } public void action(){ System.out.println("Client.action() : " + service.doSomething()); } } // ********** service ************** interface ServiceI{ public String doSomething(); } class Service3 implements ServiceI{ public String doSomething(){ return this.getClass() + ".doSomething()"; } } // ********** main ************** public class Test3{ public static void main(String[] args){ Client3 client = new Client3(); client.setService(new Service3()); client.action(); } }
Injection par interface (type 1)
Même chose que l'injection par setter, mais on demande en plus au client d'implémenter une interface.// ********** client ************** interface ServiceUser{ public void injectService(ServiceI service); } class Client4 implements ServiceUser{ private ServiceI service; public void injectService(ServiceI service){ this.service = service; // ICI injection } public void action(){ System.out.println("Client.action() : " + service.doSomething()); } } // ********** service ************** interface ServiceI{ public String doSomething(); } class Service4 implements ServiceI{ public String doSomething(){ return this.getClass() + ".doSomething()"; } } // ********** main ************** public class Test4{ public static void main(String[] args){ Client4 client = new Client4(); client.injectService(new Service4()); client.action(); } }
Remarques
- Une règle pouvant être utile : utiliser l'injection par constructeur pour les services obligatoires (sans lesquels le client ne peut pas fonctionner), et l'injection par setter pour les services optionnels (dont le client n'a pas tout le temps besoin).
-
Il existe plusieurs moyens de réaliser l'injection de dépendances :
- l'injection peut être codé en dur, comme dans la fonction
main()
des exemples. - On peut utiliser un fichier de configuration pour spécifier les types de service à utiliser (et utiliser la reflection pour construire les services).
- On peut utiliser des annotations ; moyen utilisé par JEE pour fabriquer des EJB.
- L'injection de dépendance peut permettre de faire des pools d'objets.
- l'injection peut être codé en dur, comme dans la fonction
Service locator
L'idée est d'avoir un objet (le service locator) qui sait comment créer tous les services dont une application (un client) peut avoir besoin.Le diagramme exprimant les dépendances devient le suivant : Exemple de JNDI en JEE.