Le principe de ségrégation des interfaces
Le principe de ségrégation des interfaces (Interface Segregation Principle ou ISP) est l’un des cinq principes SOLID de la programmation orientée objet. Il met l’accent sur la conception d’interfaces claires, spécifiques et cohérentes pour les clients, afin de minimiser les dépendances inutiles et de favoriser la modularité et la flexibilité du code.
Selon le principe ISP, les clients (classes, modules, ou composants) ne devraient pas être forcés d’implémenter des fonctionnalités dont ils n’ont pas besoin. Au lieu de cela, les interfaces doivent être suffisamment spécifiques pour répondre exactement aux besoins des clients spécifiques.
Prenons un cas concret :
Voici le contrat d'interface pour un lecteur de musique.Maintenant, ajoutons deux lecteurs avec des implémentations et des cibles différentes.
La première implémentation de notre contrat concerne un lecteur pour mobile. Ce lecteur support les trois fonctionnalités du contrat.
Maintenant, c’est une radio, qui ne supporte pas l’affichage des paroles. Dans ce cas, la classe Radio est obligée de mettre en œuvre la méthode displayLyrics
même si elle n’en a pas besoin. La classe Radio est forcée de fournir une implémentation vide ou de lancer une exception pour une fonctionnalité qui n’est pas pertinente pour elle. De plus, lancer une exception ici viole également le principe de Liskov, dont vous pouvez trouver une présentation dans l’article de la semaine dernière, car on ne peut pas substituer la radio à tous les MusicPlayer
sans causer de problème lors de l’appel à la méthode displayLyrics
.
Remanions un peu ce code afin de régler le problème et de respecter le principe ISP. La première étape consiste à séparer notre interface en deux.
Nous pouvons donc maintenant implémenter ces interfaces depuis nos deux lecteurs précédents.
Dans cet exemple, nous avons divisé l’interface MusicPlayer
en deux avec d’un part MusicPlayer
et d’autre part LyricsDisplay
. La classe MobilePlayer
implémente les deux interfaces, car elle a besoin des fonctionnalités de lecture de musique et d’affichage des paroles. La classe Radio implémente uniquement l’interface MusicPlayer
, car elle n’a pas besoin de la fonctionnalité d’affichage des paroles.
Ainsi, en respectant le principe ISP, nous avons créé des interfaces spécifiques qui permettent aux clients de dépendre uniquement des fonctionnalités dont ils ont besoin, évitant ainsi d’avoir à implémenter des fonctionnalités inutiles. Nos deux implémentations sont bien interchangeables partout où l’on souhaite avoir un lecteur de musique.
Ce principe s’applique particulièrement pour les langages typés statiquement (comme TypeScript). Bien que le problème puisse se retrouver également dans les langages typés dynamiquement (Ruby par exemple).
Reprenons notre exemple, mais avec Ruby cette fois-ci. Dans le cas d’un langage typé dynamiquement, on ne peut pas créer d’interface pour définir un contrat et on n’a pas de type dans nos fonctions pour nous garantir ce qu’on doit avoir en paramètre ou en retour d’une fonction. En Ruby, on considère que toute instance qui répond aux attentes d’une fonction peut être utilisée. En d’autres termes, peu importe le type de l’élément passé en paramètre, si notre fonction à besoin d’appeler la méthode foo
sur cet élément, alors toutes les instances de classes ou structure de données qui ont une méthode foo
peuvent être substituée.
Voici la première implémentation de lecteur mobile :
En maintenant l’implémentation de la radio :
Cette dernière ne propose une implémentation que pour les deux premières méthodes et ne peut donc pas afficher les paroles.
En Ruby, ces deux classes sont parfaitement interchangeables pour les méthodes play_music
et stop_music
. En revanche, ce n’est pas le cas pour display_lyrics
qui va provoquer une NoMethodError
avec une instance de Radio. Ce n’est pas forcément un problème, car il n’y a pas de classe parente ici puisqu’il n’y a pas besoin de créer un contrat dans un langage typé dynamiquement. En revanche, pour éviter les bugs, il faut beaucoup de vigilance aux développeurs afin de ne pas se retrouver à utiliser une instance de classe dans un contexte auquel elle n’est pas adaptée. C’est pour cette raison qu’en Ruby (tout comme en JavaScript) on se retrouve avec beaucoup de programmation défensive. Dans le cas présent, pour l’affichage des paroles, on pourrait tester au préalable si notre instance répond à la méthode display_lyrics
(object.respond_to?(:display_lyrics)
) avant de l’appeler.
En conclusion, on peut retenir que dans le cadre d’un langage typé statiquement, lorsqu’on définit des contrats explicites, il faut faire attention à avoir des contrats adaptés à nos clients. L’objectif étant de ne pas forcer des implémentations du contrat à produire du code inutilement. Pour un langage typé dynamiquement, on ne définit pas de contrat explicite. Donc il n’y a pas d’obligation d’implémenter des méthodes dont on n’a pas besoin. Il est plus facile de ne pas se retrouver à devoir fournir une implémentation de quelque chose sans en avoir le besoin. En revanche, il n’y a pas non plus d’assurance que le composant qu’on utilise est correcte. Il n’y a aucune garantie que les implémentations respectent bien un contrat particulier (nom et paramètre des fonctions ainsi que ce que la fonction retourne comme type de valeur). Cela met la rigueur des développeurs à rude épreuve et est souvent la cause des bugs d’une application.