Skip to content

Injection de dépendances

Concept

  • Le but de cette présentation est de faire comprendre le fonctionnement et l'intérêt de l'injection de dépendances.
  • L'intérêt est :

    • d'améliorer la modularité d'un projet ;
    • de faciliter les tests ;
    • de ne pas créer ses objets soi-même.

Présentation d'un jeu de Golf standard

Le Golfeur demande au Caddy de lui fournir le Club approprié

Le golfeur n'a pas à se soucier du club qu'il va utiliser car :

  • au début du parcours, un caddy accompagne le golfeur. Celui-ci possède un sac de golf avec plusieurs clubs.
  • Pendant le parcours, le caddy lui fournit le club adapté à la situation

De cette façon :

  • Le caddy est fournisseur de services.
  • Le golfeur est consommateur des services fournis.

Le concept d'injection de dépendances est un pattern1 qui est implémenté dans de nombreux frameworks Java parmi lesquels :

  • JavaEE avec les EJBs (Enterprise Java Beans)
  • Spring framework
  • Google Guice2 (que nous utiliserons par la suite)

Quelques explications : https://github.com/google/guice/wiki/Motivation (prenez le temps de lire cette page plus tard, elle est très instructive).

Nous allons voir comment organiser les classes sans et avec injection de dépendances.

Principe de découplage et abstraction (SANS injection de dépendances)

Le principe de découplage est important dans le développement logiciel, car il permet une bonne évolutivité du code tout en facilitant la maintenance et diminuant le risque d'effets de bord.

Interface Club (déclaration de service)

En Java, l'abstraction est gérée en déclarant des interfaces de services.

package fr.iut;

/**
 * Abstraction for all golf club
 */
public interface Club {
    /**
     * @param force     float between 0 and 1 to indicate the force vector value
     * @param direction the direction assuming North is PI/2 rad
     * @param ball      the ball to move
     */
    void shoot(final double force, final double direction, Ball ball);
}

Notez que dans une interface, toutes les méthodes sont implicitement publiques. Ainsi, on ne déclare pas le mot-clé public devant les méthodes. Pour de plus amples détails : http://docs.oracle.com/javase/tutorial/java/concepts/interface.html.

Il est nécessaire de fournir des implémentation de ce service.

Classe Putter, implémentation d'un club (Implémentation de service)

L'implémentation est effectuée comme suit. À noter qu'elle est placée dans un sous-package afin de mieux illustrer le couplage qui apparaîtra dans la classe Caddy.

package fr.iut.club;

import fr.iut.Ball;
import fr.iut.Club;
import java.awt.geom.Point2D;
/**
 * Simple putter club implementation.
 */
public class Putter implements Club {
    /**
     * inner constant used to compute new position after shoot
     */
    private final static int DISTANCE_MAX = 10;
    public void shoot(final double force, final double direction, final Ball ball) {
        double x = ball.getPosition().getX();
        double y = ball.getPosition().getY();
        x += (force * DISTANCE_MAX) * Math.sin(direction);
        y += (force * DISTANCE_MAX) * Math.cos(direction);
        ball.setPosition(new Point2D.Double(x, y));  }
}

D'autres implémentations sont nécessaires pour les autres services (clubs).

Classe Ball

La classe Ball représente une balle de golf à une position sur le parcours (attribut position).

package fr.iut;

import java.awt.geom.Point2D;
/**
 * Golf ball class.
 */
public class Ball {
    private final Point2D position = new Point2D.Double();
    /**
     * default constructor with a position of (0,0)
     */
    public Ball() {
        position.setLocation(0, 0);
    }
    public Point2D getPosition() {
        return position;
    }
    public void setPosition(final Point2D position) {
          this.position.setLocation(position);
    }
}

Classe du Caddy (Fournisseur de services)

La classe du caddy :

  • possède les différents clubs ;
  • sait quel club utiliser en fonction des conditions.
package fr.iut;

import fr.iut.club.Putter;
import fr.iut.club.Wood;

/**
 * A caddy has several clubs and knows which club to use depending on conditions
 */
public class Caddy {
    private Club putter = new Putter();
    private Club wood = new Wood();

    /**
     * default empty constructor *
     */
    public Caddy() {  }

    /**
     * Return
     * @param conditions
     * @return
     */
    public Club getClub(final Conditions conditions) {
        switch (conditions) {
            case GREEN:
                return putter;
            case FAIRWAY:
                return wood;
            default:
                return putter;
        }
    }
}

À noter le couplage entre Caddy et les deux clubs Putter et Wood. La dépendance est marquée à deux endroits :

  1. dans la section d'imports :

import fr.iut.club.Putter;
import fr.iut.club.Wood;
2. dans les attributs, sur le new :

private Club putter = new Putter();
private Club wood = new Wood();

Enum des Conditions

La classe d'enum des différentes conditions de jeu.

1
2
3
4
5
6
7
8
package fr.iut;

/* Golf terrain conditions*/
public enum Conditions {
           GREEN,
           FAIRWAY,
           BUNKER
}

Classe Player

    package fr.iut;

        /**
        * A Golf player has a ball and a Caddy.
        */
    public class Player {
    private final String name;
    private final Ball ball = new Ball();
    private final Caddy caddy;

    /**
     * Instanciate a new Player.
     *
     * @param name  cannot be null
     * @param caddy cannot be null
     */
    public Player(final String name, final Caddy caddy) {
        if (name == null || caddy == null) {
            throw new IllegalArgumentException("Null not allowed");   }
        this.name = name;
        this.caddy = caddy; }

    public void play(final double force, final double direction, 
                 final Conditions conditions) {
        Club club = caddy.getClub(conditions);
        club.shoot(force, direction, this.ball);
    }

    @Override
    public String toString() {
        return String.format("Player '%s' ball is at (%2f, %2f)", 
            this.name, this.ball.getPosition().getX(), 
              this.ball.getPosition().getY());
    }

    public Ball getBall() {return ball; }
}

Classe App

Pour finir, le point d'entrée principal du programme qui fait avancer une balle de golf sur un parcours.

package fr.iut;

/**
 * Basic implementation
 */
public class App {

    public static void main(String[] args) {
        Caddy caddy = new Caddy();
        Player player = new Player("John", caddy);
        System.out.println(player);
        player.play(0.8, Math.PI / 2, Conditions.FAIRWAY);
        System.out.println(player);
        player.play(0.1, Math.PI / 2, Conditions.GREEN);
        System.out.println(player);
    }
}

Diagramme de classes sans injection de dépendances


Exercice

Objectif : Coder le mini-projet de Golf (15 minutes) qui servira de base à la version avec injection de dépendances.

  1. À l'aide de Maven (dans le dossier injection du TP)
    • créer la structure du projet (cf. cours Prérequis et archetype)
    • nom de projet: golf
  2. Importer le projet dans IntelliJ (cf. cours Prérequis)
  3. Copier l'interface Club
  4. Copier l'implémentation Putter qui fait avancer la balle de golf de 10 mètres
  5. Coder l'implémentation Wood qui fait avancer la balle de golf de 100 mètres
  6. Coder les autres classes du projet
  7. Dans une classe App, coder la fonction main
  8. Commiter/pousser

C'est rapide, tout le code est dans le support !


Explications

La classe Caddy dans l'exemple précédent joue 2 rôles :

  1. le rôle de conteneur en cachant les implémentations de Club (en attribut)
  2. le rôle de résolveur de dépendances en connaissant les implémentations d'interfaces à utiliser selon les conditions du terrain

L'abstraction est illustrée dans la classe Player où il n'y a aucune trace des classes Putter et Wood ! Les classes concrètes sont manipulées à travers l'interface Club.

Pour autant, il y a un couplage au niveau de la classe Caddy puisque cette dernière est obligée d'instancier les classes concrètes.

Utilisation d'injection de dépendances

Injection de dépendances avec Google Guice

  • Google Guice est un framework d'injection de dépendances.
  • L'injection permet de supprimer le couplage entre le golfeur et son caddy ainsi qu'entre le caddy et ses clubs.
  • Techniquement, l'opération est rendue possible grâce à la JVM qui permet la manipulation du bytecode à la volée.

Fonctionnement de l'injection de dépendances

L'instanciation des objets qui ont des dépendances va être déléguée au conteneur Guice. C'est à ce dernier que l'on va devoir s'adresser pour récupérer des instances.

Classe App avec google Guice

package fr.iut;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class App {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new MainModule());

        Caddy caddy = injector.getInstance(Caddy.class);
        Player player = new Player("John", caddy);

        System.out.println(player);
        player.play(0.8, Math.PI / 2, Conditions.FAIRWAY);
        System.out.println(player);
        player.play(0.1, Math.PI / 2, Conditions.GREEN);
        System.out.println(player);
    }
}

Référence :

Classe MainModule

Tout le code de résolution des dépendances est dans la classe MainModule qui indique quelle classe utiliser pour quel service.

package fr.iut;

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import fr.iut.club.Putter;
import fr.iut.club.Wood;

/**
 * The Guice driven Container
 */
public class MainModule extends AbstractModule {
    @Override
    protected final void configure() {
        bind(Caddy.class);
        bind(Club.class)
            .annotatedWith(Names.named("Putter"))
            .to(Putter.class);
        bind(Club.class)
            .annotatedWith(Names.named("Wood"))
            .to(Wood.class);
    }
}

Référence :

Classe Caddy

Dès lors, l'utilisation de Guice permet d'injecter les dependances comme dans le code ci-dessous.

package fr.iut;

import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * A caddy has several clubs and know which club to use depending on conditions
 */
public class Caddy {
    @Inject
    @Named("Putter")
    private Club putter;

    @Inject
    @Named("Wood")
    private Club wood;

    public Caddy() {  }

    /**
     * @param conditions
     * @return the appropriate club according to conditions
     */
    public Club getClub(final Conditions conditions) {
        switch (conditions) {
            case GREEN:
                return putter;
            case FAIRWAY:
                return wood;
            default:
                return putter;
        }  
    }
}

Référence :

  • On note qu'il n'y a plus aucun couplage avec une implémentation de Club dans Caddy (Suppression des import et new).
  • On peut ajouter de nouvelles implémentations de Club sans recompiler Caddy.

Diagramme de classes avec injection de dépendances


Exercice

Objectif : Mise en place d'un système d'injection de dépendances. Intégrer google Guice au mini-projet de Golf (30 minutes)

  1. Pensez à commiter et pousser la version précédente du golf
  2. Rajouter la dépendance dans le pom.xml
<dependencies>
    ...
    <dependency>
        <groupId>com.google.inject</groupId>
        <artifactId>guice</artifactId>
        <version>5.0.1</version>
    </dependency>
    ...
</dependencies>
  1. Rajouter la classe MainModule
  2. Modifier la classe Caddy
  3. Modifier le main pour exécuter ce programme
  4. Coder une nouvelle implémentation de l'interface Club nommé PutterExperimental.
  5. Ajouter une deuxième classe avec un main et un MainModuleExperimental pour utiliser la nouvelle implémentation de Club (ExperimentalPutter) pour illustrer la possibilité de créer une autre application qui utilise un autre type de Putter sans avoir à modifier Caddy.
  6. Commiter régulièrement
  7. Ajouter un tag injection à la fin du TD
  8. Pousser

Nous verrons plus tard comment lancer ce type d'application manuellement. Contentez-vous d'utiliser l'IDE pour le moment.


Les architectures en couches ont un recours massif à l'injection de dépendances. Elles limitent -entre autres- les risques de dépendances circulaires, signe d'une erreur de conception.