Dernière mise à jour:

Le principe de ségrégation des interfaces

Cédric Gérard
Cédric Gérard Code

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 :

        
interface MusicPlayer {
    playMusic(): void;
    stopMusic(): void;
    displayLyrics(): void;
}
        
    
TypeScript
Voici le contrat d'interface pour un lecteur de musique.

Maintenant, ajoutons deux lecteurs avec des implémentations et des cibles différentes.

        
class MobilePlayer implements MusicPlayer {
    public playMusic(): void {
        console.log("Playing music...");
    }

public stopMusic(): void { console.log("Stopping music..."); }
public displayLyrics(): void { console.log("Displaying lyrics..."); } }
TypeScript

La première implémentation de notre contrat concerne un lecteur pour mobile. Ce lecteur support les trois fonctionnalités du contrat.

        
class Radio implements MusicPlayer {
    playMusic(): void {
        console.log("Playing music...");
    }

stopMusic(): void { console.log("Stopping music..."); }
displayLyrics(): void { throw new Error("Radio doesn't support displaying lyrics."); } }
TypeScript

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.

        
interface MusicPlayer {
    playMusic(): void;
    stopMusic(): void;
}

interface LyricsDisplay { displayLyrics(): void; }
TypeScript

Nous pouvons donc maintenant implémenter ces interfaces depuis nos deux lecteurs précédents.

        
class MobilePlayer implements MusicPlayer, LyricsDisplay {
    playMusic(): void {
        console.log("Playing music...");
    }

stopMusic(): void { console.log("Stopping music..."); }
displayLyrics(): void { console.log("Displaying lyrics..."); } }
class Radio implements MusicPlayer { playMusic(): void { console.log("Playing music..."); }
stopMusic(): void { console.log("Stopping music..."); } }
TypeScript

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 :

        
class MobilePlayer

def play_music puts "Playing music..." end
def stop_music puts "Stopping music..." end
def display_lyrics puts "Displaying lyrics..." end
end
Ruby

En maintenant l’implémentation de la radio :

        
class Radio
 
def play_music puts "Playing music..." end
def stop_music puts "Stopping music..." end
end
Ruby

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.

Cédric Gérard

Cédric Gérard

Je suis dans l'informatique depuis tout jeune. D'abord intéressé par le hardward (montage, overcloking), j'ai mis du temps à trouver ma voie. Je suis tombé dans le développement en 2007, je n'ai jamais arrêté depuis..

Aujourd'hui, je suis développeur web avec une plus grande appétence pour le backend. J’accorde beaucoup d’attention à la valeur apportée aux utilisateurs finaux. On ne réalise pas d'application que pour se faire plaisir, après tout.

Je mets aussi un point d'honneur à livrer du code de qualité en m'appuyant sur les bonnes pratiques du développement logiciel et je défends les valeurs du software craftmanship.

L'agilité est également un élément essentiel pour un travail fiable et efficace. Je ne parle pas de méthode, mais de l'état d'esprit prôné par l'agilité.

J'aime partager mes compétences et j'ai une appétence particulière pour l'encadrement des développeurs juniors.

Je suis également en quête de sens, aucune technologie étant une fin en elle-même, j'ai besoin de savoir pourquoi je travaille et qu'elle est la valeur produite.

Articles en relation

Le principe d'injection de dépendance

Le principe de substitution de Liskov

Le principe Ouvert/Fermé