ISP (Interface Segregation Principle) est le quatrième principe SOLID, qui stipule : les clients ne doivent pas dépendre de méthodes qu'ils n'utilisent pas. Le principe a été formulé par Robert Martin dans le contexte de la conception d'interfaces pour les systèmes orientés objet. Comme décrit dans le livre Clean Architecture (2017), le principe de ségrégation des interfaces exige de créer des interfaces étroitement spécialisées au lieu d'une seule universelle, ce qui réduit le couplage et simplifie la réalisation de modifications.
Points clés
ISP (Interface Segregation Principle) est le principe de ségrégation des interfaces qui interdit de créer des interfaces « grasses » avec des méthodes que tous les clients n'utilisent pas. Au lieu d'une interface avec une douzaine de méthodes, plusieurs petites interfaces sont conçues, chacune pour son groupe de clients.
Le principe a été introduit par Robert Martin comme solution au problème de la « pollution des interfaces », lorsqu'une classe est obligée d'implémenter des méthodes dont elle n'a pas besoin simplement parce qu'elles sont déclarées dans une interface commune. Dans les langages typés statiquement, cela entraîne des implémentations vides ou des levées d'exceptions — un signe direct de violation de l'ISP.
ISP et SRP se complètent : SRP concerne la responsabilité de la classe, ISP concerne les contrats d'interfaces. SRP dit « une classe — une raison de changer », ISP dit « une interface — un scénario client ». Ensemble, ils forment une architecture modulaire où chaque élément du système a des limites claires.
Fat Interface — une interface contenant plus de méthodes qu'un client spécifique n'en a besoin. Par exemple, une interface Worker avec les méthodes work, eat, sleep. Un robot travailleur ne devrait pas implémenter eat et sleep, mais y est forcé. La solution est de diviser en Workable, Eatable, Sleepable. Chaque client obtient exactement ce dont il a besoin.
Dans le développement mobile, les interfaces grasses se trouvent dans les protocoles délégués et DataSource. Un protocole peut contenir des méthodes pour deux scénarios différents (édition + affichage), même si un écran spécifique n'en utilise qu'un seul.
Implémenter ISP commence par analyser les clients de chaque interface. Si deux clients utilisent des ensembles différents de méthodes d'une même interface, l'interface doit être divisée. Chaque nouvelle interface regroupe les méthodes qui sont appelées ensemble dans un même scénario.
Le mécanisme de division : l'interface d'origine est découpée en plusieurs interfaces étroites, chacune héritant de la partie commune (si elle existe). Les clients passent à la dépendance de l'interface étroite nécessaire au lieu de l'interface générale. Les classes qui implémentaient l'interface d'origine n'implémentent désormais que les interfaces étroites dont elles ont réellement besoin.
Une clarification importante : le degré de division est déterminé par le nombre de clients et leurs scénarios. ISP n'exige pas une division maximale (micro-interfaces avec une méthode chacune). Cela entraînerait une complexité excessive. L'objectif est d'éliminer la dépendance des clients vis-à-vis des méthodes inutiles, pas de minimiser la taille de chaque interface.
Les principaux signes de violation de l'ISP incluent : des classes implémentant une interface avec des méthodes vides (implémentation factice), le lancement de UnsupportedOperationException dans les implémentations, un grand nombre de paramètres ou de types de retour non utilisés par certains clients, et des modifications fréquentes de l'interface n'affectant que certains clients.
Dans le développement Android, un exemple typique de violation de l'ISP est l'interface OnItemClickListener, qui inclut des méthodes pour le clic, le clic long et le balayage. Si un écran spécifique n'utilise que le clic, les méthodes restantes restent vides. La solution est de diviser en OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
Dans le développement iOS, la violation de l'ISP se manifeste dans les délégués UIKit : un protocole contient des méthodes pour différents états du composant. UITableViewDelegate inclut des méthodes pour l'affichage, la sélection, l'édition et l'action de balayage. Les développeurs implémentent souvent le protocole entier avec une douzaine de méthodes vides. La division en plusieurs protocoles par groupes de responsabilité résout le problème.
Le problème n'est pas seulement esthétique. Lorsqu'une interface change (une nouvelle méthode est ajoutée), toutes les classes d'implémentation doivent être mises à jour, même celles qui n'ont pas besoin de la nouvelle méthode. Dans le développement mobile avec des dizaines d'écrans, cela entraîne des modifications en cascade. ISP isole chaque client des changements qui ne le concernent pas.
La violation implicite de l'ISP se produit via les paramètres de configuration. Si une méthode accepte un objet avec de nombreux champs et que le client n'en utilise que 2-3, c'est un signal pour diviser. Alternative : plusieurs méthodes spécialisées avec un ensemble minimal de paramètres.
Dans le développement Android, l'ISP est violé lors de l'utilisation d'un seul SharedPreferencesManager pour lire et écrire tous les paramètres de l'application. Un Fragment qui a seulement besoin de lire le thème obtient une dépendance vis-à-vis d'un gestionnaire global avec une douzaine de méthodes pour différents types de données. La division en ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — l'application d'ISP au niveau des services de configuration. Chaque fournisseur contient exactement les méthodes dont ses clients ont besoin.
Considérons un exemple Android avec une interface pour travailler avec des données. Violation d'ISP : une interface pour toutes les opérations CRUD, même si tous les clients n'ont pas besoin de toutes les opérations.
// Violation d'ISP : interface grasse
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Après application d'ISP : interfaces étroites
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel ne dépend pas des méthodes d'écriture
class ReadOnlyViewModel(
private val reader: UserReader
)
Un exemple iOS avec séparation de protocoles pour travailler avec des médias :
// Violation d'ISP : un protocole pour tout le travail avec les médias
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// Après ISP : séparation en protocoles par responsabilité
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModel ne dépend pas des méthodes de téléchargement
class PlayerViewModel {
private let player: MediaPlayer
}
Conclusion pratique : ISP protège les clients des modifications dans les parties non liées de l'interface. Diviser UserRepository en UserReader et UserWriter signifie que les modifications dans save n'affectent pas ReadOnlyViewModel, et vice versa. Chaque client est isolé des fonctionnalités qu'il n'utilise pas et ne nécessite pas de modifications lorsque d'autres parties du système sont modifiées.
ISP et SRP — une paire naturelle. SRP définit qu'une classe doit avoir une seule raison de changer. ISP applique la même logique aux interfaces : une interface doit servir un scénario client. Une classe peut implémenter plusieurs interfaces étroites (chacune correspondant à une responsabilité), ce qui est plus propre qu'une interface grasse avec plusieurs responsabilités.
ISP et OCP sont également liés : les interfaces étroites sont plus faciles à étendre. Ajouter une nouvelle méthode à une interface étroite n'affecte que ses clients. Ajouter une méthode à une interface grasse affecte tous les clients — violant potentiellement l'OCP si les clients sont obligés de modifier leur implémentation.
ISP et DIP travaillent ensemble : DIP exige la dépendance d'abstractions. ISP rend ces abstractions étroites et ciblées. Dépendre d'une interface large reste une dépendance d'une abstraction, mais une « mauvaise » abstraction du point de vue d'ISP. Quatre principes (SRP, OCP, ISP, DIP) forment la « pyramide de modularité » : SRP et ISP définissent les limites, OCP et DIP définissent les façons d'extension et de couplage.
L'architecture de composants dans les projets mobiles (modules, fonctionnalités, couches) bénéficie d'ISP au niveau des API publiques. Chaque module exporte des interfaces étroites pour ses consommateurs, au lieu d'une seule façade commune. Cela permet de modifier l'implémentation interne du module sans affecter les consommateurs qui n'utilisent qu'une partie de ses fonctionnalités.
Dans les projets Android avec Clean Architecture, ISP est appliqué aux UseCases : chaque UseCase est une interface séparée avec une seule méthode invoke ou execute. Le client (ViewModel) ne dépend que du UseCase dont il a besoin, plutôt que d'un référentiel entier. Cela rend les dépendances transparentes et testables.
Questions fréquentes
Oui, une division excessive est possible. ISP n'exige pas une interface par méthode. Le critère est : existe-t-il un client qui n'a besoin que d'une partie des méthodes de l'interface ? Si tous les clients utilisent toutes les méthodes, l'interface n'a pas besoin d'être divisée. Le niveau optimal de division est déterminé par des scénarios d'utilisation réels.
ISP au niveau des paramètres signifie : une fonction ne doit pas accepter d'objets avec un grand nombre de champs si elle n'en utilise qu'une partie. Au lieu de cela, seules les données nécessaires doivent être transmises ou des interfaces spécialisées doivent être utilisées (par exemple, une interface Renderable au lieu d'un User complet).
LSP concerne l'héritage correct et la compatibilité comportementale des sous-types. ISP concerne la conception d'interfaces : les clients ne doivent pas dépendre de méthodes qu'ils n'utilisent pas. LSP répond à la question « une sous-classe peut-elle être utilisée à la place de la classe de base ? », ISP répond « le client a-t-il besoin de l'interface entière ? »
Les interfaces étroites simplifient la création d'objets mock : le test crée un mock avec une ou deux méthodes, pas avec une douzaine. Moins une interface a de méthodes, plus il est facile de simuler son comportement. Cela réduit la charge cognitive du développeur de test et diminue la probabilité d'erreurs dans la logique mock.
Si l'interface est stable et que tous les clients utilisent toutes les méthodes, la division est redondante. Un exemple typique : les protocoles UIKit conçus par Apple. Les diviser est risqué car UIKit s'attend à une implémentation complète du délégué. Dans de tels cas, la violation d'ISP est justifiée par la stabilité de l'API.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi