L’inversion de contrôle

À la base du Spring Framework, on trouve un unique principe de conception : l’inversion de contrôle.

L’inversion de contrôle est une façon de concevoir l’architecture d’une application en se basant sur le mécanisme objet de l’injection de dépendance.

Note

L’inversion de contrôle est liée étroitement au principe d’inversion de dépendance. Le principe d’inversion de dépendance est un des cinq principes fondamentaux de la conception objet identifiés par Robert C. Martin et résumés par l’acronyme SOLID. Le D de SOLID signifie Dependency inversion principle.

L’injection de dépendance

L’injection de dépendance est un mécanisme simple à mettre en œuvre dans le cadre de la programmation objet et qui permet de diminuer le couplage entre deux ou plusieurs objets.

Dans un programme objet, les objets ont des relations les uns avec les autres. Imaginons un exemple simple d’un objet qui permet de réaliser une réservation d’une salle de réunion. Une implémentation en Java pourrait ressembler à ceci :

Un exemple d’implémentation de la classe ReservationSalleService
public class ReservationSalleService {

  private ReservationSalleDao reservationSalleDao;

  public void reserver(ReservationSalle reservationSalle) {
    // faire un traitement nécessaire
    // (par exemple la validation de la réservation)

    // sauvegarder la réservation
    reservationSalleDao.sauver(reservationSalle);
  }

}

Dans l’exemple ci-dessus, nous avons une répartition assez classique des responsabilités avec trois classes :

ReservationSalleService

Représente le service à rendre par l’application. On peut imaginer plusieurs traitements à réaliser lors de l’appel à la méthode reserver(ReservationSalle) et, au final, la réservation est sauvegardée dans un système de base de données.

ReservationSalleDao

Représente l’accès à un système de base de données. Le suffixe Dao signifie Database access object et permet d’indiquer au lecteur que cette classe a pour responsabilité d’interagir avec un système de base de données. La méthode sauver(ReservationSalle) appelée dans notre exemple doit sans doute réaliser une insertion en SQL.

ReservationSalle

Réprésente la notion de réservation d’une salle de réunion.

Le code ci-dessus, nous conduit à dire que pour réaliser son travail, la classe ReservationSalleService est dépendante d’objets des classes ReservationSalleDao et ReservationSalle. L’objet de type ReservationSalleDao est particulièrement intéressant car il est déclaré comme attribut de la classe. Cela signifie que cet objet fait partie de l’état de notre service et nous conduit à nous poser un problème fondamental en architecture logicielle : quelle partie du programme doit être responsable de la création de cette instance de ReservationSalleDao.

Une réponse simple consisterait à dire que chaque objet de la classe ReservationSalleService est responsable de créer l’objet ReservationSalleDao. En programmation orientée objet, ce type de comportement est très facile à implémenter avec un constructeur.

Création de la dépendance dans le constructeur
public class ReservationSalleService {

  private ReservationSalleDao reservationSalleDao;

  public ReservationSalleService() {
    reservationSalleDao = new ReservationSalleDao();
  }

  public void reserver(ReservationSalle reservationSalle) {
    // faire un traitement nécessaire
    // (par exemple la validation de la réservation)

    // sauvegarder la réservation
    reservationSalleDao.sauver(reservationSalle);
  }

}

Cette solution, bien que simple, est problématique. En faisant cela, nous introduisons un couplage fort entre la classe ReservationSalleService et la classe ReservationSalleDao. D’une part, il n’est pas possible de considérer ReservationSalleDao comme une abstraction ou une interface puisque le service doit lui-même spécifier le type concret de l’attribut reservationSalleDao. D’autre part, il est possible que la création d’un objet de type ReservationSalleDao nécessite de passer des paramètres. Par exemple, puisqu’il s’agit d’un objet représentant un accès à une base de données, peut-être qu’il attend comme paramètres de constructeur l’adresse de la base de données, le login, le mot de passe d’accès… Tout un ensemble d’informations que la classe ReservationSalleService n’a sans doute pas à connaître. Enfin, un objet de type ReservationSalleDao a un état interne à maintenir comme, par exemple, une ou plusieurs connexions à une base de données. Il peut être plus judicieux de partager la même instance d’un tel objet à travers le programme afin de mieux gérer les ressources de l’application. Dans notre exemple, l’objet de type ReservationSalleDao est créé spécifiquement pour le service et sa durée de vie est liée à celle du service. En UML, on parle de composition pour désigner ce type fort de relation.

Tous ces problèmes potentiels devraient nous amener à relativiser la simplicité d’une telle solution et à chercher une façon alternative d’établir la dépendance entre les objets de types ReservationSalleService et ReservationSalleDao.

Une approche plus élaborée se base sur le principe du service locator conçu comme un singleton. Nous pourrions créer une classe ReservationSalleDaoLocator qui agirait comme une classe outil pour accéder à une instance d’un objet ReservationSalleDao en appelant la méthode de classe getDao().

Utilisation du modèle du service locator
public class ReservationSalleService {

  public void reserver(ReservationSalle reservationSalle) {
    // faire un traitement nécessaire
    // (par exemple la validation de la réservation)

    // sauvegarder la réservation
    ReservationSalleDao reservationSalleDao = ReservationSalleDaoLocator.get();
    reservationSalleDao.sauver(reservationSalle);
  }

}

Cette implémentation élimine tous les problèmes évoqués pour la précédente implémentation. La classe ReservationSalleService n’a plus à connaître le type concret de l’objet et ReservationSalleDao peut être implémentée sous la forme d’une abstraction ou d’une interface. La classe ReservationSalleService n’est plus responsable directement de la création de l’objet, elle peut donc ignorer les paramètres nécessaires à la construction d’un tel objet. Il est possible d’utiliser la même instance de l’objet DAO dans d’autres objets. La solution d’un singleton de type service locator est très largement utilisée pour tous ces avantages… mais ce modèle n’est pas exempt de critiques.

Le recours au modèle du service locator comme singleton n’est pas la méthode la plus optimale pour représenter un lien de dépendance. Si nous reprenons, l’exemple ci-dessus, il est évident que la classe ReservationSalleService entretient une dépendance avec la classe ReservationSalle puisqu’elle attend un paramètre de ce type à sa méthode reserver(ReservationSalle). Par contre, même si la dépendance à ReservationSalleDao existe toujours, elle est plus difficile à saisir car elle n’apparaît que dans le code de la méthode reserver(ReservationSalle). À l’usage, ce type de solution rend donc le code difficile à lire et difficile à tester. Il repose trop sur un principe d’effet de bord.

Une troisième solution permet de conserver les avantages de l’implémentation précédente tout en rendant le lien de dépendance explicite entre les classes ReservationSalleService et ReservationSalleDao : il suffit de réaliser une injection de dépendance par le constructeur :

Injection de la dépendance par le constructeur
public class ReservationSalleService {

  private ReservationSalleDao reservationSalleDao;

  public ReservationSalleService(ReservationSalleDao reservationSalleDao) {
    this.reservationSalleDao = reservationSalleDao;
  }

  public void reserver(ReservationSalle reservationSalle) {
    // faire un traitement nécessaire
    // (par exemple la validation de la réservation)

    // sauvegarder la réservation
    reservationSalleDao.sauver(reservationSalle);
  }

}

On dit que la classe ReservationSalleService reçoit par injection une instance de ReservationSalleDao. Le lien de dépendance est clairement indiqué par la signature du constructeur et l’attribut reservationSalleDao.

Cette implémentation est préférable aux précédentes en repoussant le problème de la création d’un objet de type ReservationSalleDao en dehors de la classe ReservationSalleService. Mais elle ne résout pas complètement le problème puisque nous imaginons qu’il va falloir qu’une autre partie du programme crée une instance de ReservationSalleDao et une instance de ReservationSalleService en passant l’objet DAO en paramètre. C’est précisément la fonction du Spring Framework de réaliser ce type d’opérations.

Note

En java, il est également possible de réaliser une injection de propriété à l’aide d’un setter :

Injection de la dépendance par setter
public class ReservationSalleService {

  private ReservationSalleDao reservationSalleDao;

  public void setReservationSalleDao(ReservationSalleDao reservationSalleDao) {
    this.reservationSalleDao = reservationSalleDao;
  }

  public void reserver(ReservationSalle reservationSalle) {
    // faire un traitement nécessaire
    // (par exemple la validation de la réservation)

    // sauvegarder la réservation
    reservationSalleDao.sauver(reservationSalle);
  }

}

D’un point de vue conception, une injection par setter indique que la dépendance est optionnelle puisqu’il est possible de créer et d’utiliser un objet sans réaliser l’injection.

L’inversion de contrôle

L’inversion de contrôle (Inversion of control ou IoC) est un principe d’architecture conduisant à inverser le flux de contrôle par rapport au développement traditionnel.

Habituellement, si je veux développer un programme qui utilise des bibliothèques tierces, je vais appeler les objets ou les fonctions de ces bibliothèques dans mon programme. Le flux de contrôle est géré par mon propre code qui doit réaliser les appels au code tiers.

Dans une approche IoC, le flux de contrôle est orienté du code tiers vers le code de mon application. Le code que je fournis sera sous le contrôle du code tiers. L’IoC est en fait le principe qui est mis en application par la plupart des frameworks. On peut même dire que c’est principalement ce qui distingue un framework d’une bibliothèque. Le framework fournit une ossature, une charpente (d’où son nom) à mon application et je ne dois fournir que le code spécifique qui doit être conforme à ce que le framework attend. Par exemple, en fournissant des classes qui héritent de telles classes ou qui implémentent telles interfaces qui sont définies par le framework.

Donc le Spring Framework n’est pas fondamentalement différent des autres frameworks… sauf qu’il met en pratique le principe de l’IoC dans sa forme la plus générale et la moins intrusive en proposant la notion de conteneur IoC.

Notion de conteneur IoC

Pour pouvoir être mise en pratique, l’inversion de contrôle implique l’existence d’un composant supplémentaire. Dans l’exemple que nous avons pris précédemment, un code tiers doit être responsable de créer une instance des classes ReservationSalleDao et ReservationSalleService. Il faut également que ce composant soit capable de réaliser l’injection de l’objet de type ReservationSalleDao.

La construction des objets de notre application va être déléguée à ce composant que l’on appelle un conteneur IoC (IoC container). Ce conteneur accueille un ensemble d’objets dont il a la responsabilité de gérer le cycle de vie. Le Spring Framework est avant tout un conteneur IoC. On peut résumer le rôle du Spring Framework en disant qu’il est responsable de la création des objets qui constituent l’ossature de notre application et qu’il s’assure que les dépendances entre eux sont correctement créées.

Comme son principe reste général, il va être possible d’utiliser le Spring Framework pour des types d’application très divers. Il faut simplement pouvoir laisser au Spring Framework la possibilité d’initialiser l’application au lancement en créant le conteneur IoC. Dans la terminologie du Spring Framework, le conteneur IoC est constitué d’un ou de plusieurs contextes d’application.

Nous allons voir au chapitre suivant comment créer un contexte d’application avec le Spring Framework.