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 :
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éthodesauver(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.
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()
.
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 :
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 :
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.