Principe ISP en Software Craftsmanship, Guide Pratique avec Exemples Java

Par KamangaMar 27, 202411 mins de lecture

Sur une mission récente dans la banque, j'ai retrouvé une interface Service qui rassemblait 23 méthodes. Toutes les classes qui l'implémentaient en ignoraient au moins la moitié. À chaque ajout de fonctionnalité, le code mort prolifère et le moindre changement casse des implémentations qui n'avaient rien demandé.

C'est la signature classique d'une mauvaise conception d'interface, et le principe de séparation d'interface (ISP) attaque exactement ce problème. Quatrième des cinq principes SOLID, ISP impose une règle simple : une interface ne doit jamais forcer une classe à implémenter des méthodes dont elle n'a pas besoin.

Dans le secteur financier, ce type d'interface "fourre-tout" est endémique : j'en ai croisé sur la plupart de mes missions. Robert C. Martin, dans Agile Software Development: Principles, Patterns, and Practices, formalise d'ailleurs ISP comme l'un des piliers d'une architecture orientée objet saine. Cet article montre comment l'appliquer en Java, exemples à l'appui, avec les erreurs typiques à éviter.


Pourquoi respecter le principe ISP ?

Le principe de séparation d’interface (ISP) fait partie des cinq principes SOLID, qui sont des lignes directrices destinées à rendre notre code plus modulable, maintenable et évolutif. ISP stipule qu’une interface ne doit pas forcer une classe à implémenter des méthodes dont elle n’a pas besoin.

Mais pourquoi est-ce si important ?

1. Faciliter la maintenance et l’évolution du code

Avec une interface trop large, le moindre ajout impacte toutes les classes qui l'implémentent, même celles qui ne sont pas concernées. Prenez une interface Employe avec calculerSalaire() et gererProjets() : toutes les classes qui l'implémentent, développeur comme responsable de projet, devront fournir une implémentation pour les deux méthodes, même si l'une est inutile pour elles.

Résultat : chaque changement dans cette interface déclenche des bugs inattendus et alourdit l'évolution du code.

Tip : si plusieurs classes implémentent une interface sans utiliser toutes ses méthodes, c'est un signal clair que cette interface est trop large. Découpez-la.

2. Favoriser la réutilisabilité

Une interface spécifique, centrée sur une seule tâche, ouvre la voie à la réutilisabilité. Si vous divisez Employe en deux interfaces ciblées (Salarie et Manager), chaque classe n'implémente que ce dont elle a besoin. Le code reste propre et les classes deviennent réutilisables sans risquer de casser le reste.

3. Réduire le couplage

Respecter ISP réduit aussi le couplage entre les différentes parties du code. Une classe qui dépend d'une interface large se retrouve liée à plusieurs fonctionnalités, ce qui complique sa modification ou son remplacement. Avec des interfaces ciblées, les classes sont moins couplées et chacune se modifie sans casser le reste du système.

4. Améliorer les tests unitaires

Les interfaces ciblées simplifient les tests unitaires. Une classe qui dépend uniquement des méthodes qui la concernent se mocke en quelques lignes. Avec une interface trop large, vous vous retrouvez à gérer des méthodes inutiles dans chaque test, ce qui alourdit la suite.

Alerte : Des interfaces larges et mal segmentées augmentent le couplage, ce qui rend vos tests plus difficiles à écrire et à maintenir.


Comment appliquer ISP en Java ?

Le principe de séparation d’interface peut sembler abstrait à première vue, mais il est très facile à comprendre une fois qu’on voit comment il s’applique dans un projet. Voici quelques exemples concrets en Java qui illustrent comment respecter ce principe.

Exemple 1 : Une interface trop large

Prenons une interface Employe qui contient plusieurs méthodes. Cette interface est mal conçue car elle impose à toutes les classes qui l’implémentent de définir des méthodes dont elles n’ont peut-être pas besoin :

public interface Employe {
    void travailler();
    void gererProjets();
    void calculerSalaire();
}

Dans ce cas, une classe Developpeur doit implémenter toutes ces méthodes, même si un développeur n’a pas à gérer des projets ou à calculer des salaires.

public class Developpeur implements Employe {
    @Override
    public void travailler() {
        System.out.println("Le développeur écrit du code.");
    }

    @Override
    public void gererProjets() {
        // Non pertinent pour un développeur.
    }

    @Override
    public void calculerSalaire() {
        // Non pertinent pour un développeur.
    }
}

Ici, Developpeur se retrouve à implémenter des méthodes inutiles, ce qui est une violation directe du principe ISP.

Alerte : Si vous vous retrouvez à laisser des méthodes vides ou à lever des exceptions UnsupportedOperationException, c'est un signal clair qu'une interface est trop large.

Exemple 2 : Appliquer le principe ISP avec des interfaces spécifiques

Pour corriger cette conception, nous devons diviser l'interface Employe en plusieurs interfaces plus spécifiques. Chaque interface doit refléter un comportement précis, que seules les classes pertinentes implémenteront.

public interface Travailleur {
    void travailler();
}

public interface Manager {
    void gererProjets();
}

public interface Salarie {
    void calculerSalaire();
}

Maintenant, la classe Developpeur n’a plus besoin d’implémenter des méthodes qui ne sont pas pertinentes pour elle. Elle n’implémente que l’interface Travailleur :

public class Developpeur implements Travailleur {
    @Override
    public void travailler() {
        System.out.println("Le développeur écrit du code.");
    }
}

De même, un ResponsableProjet pourrait implémenter l’interface Manager et l’interface Travailleur s'il gère des projets tout en travaillant :

public class ResponsableProjet implements Manager, Travailleur {
    @Override
    public void travailler() {
        System.out.println("Le responsable de projet supervise les tâches.");
    }

    @Override
    public void gererProjets() {
        System.out.println("Le responsable de projet gère les équipes.");
    }
}

Cette approche respecte parfaitement le principe ISP, car chaque classe n'implémente que les méthodes dont elle a réellement besoin.

Tip : Si vous identifiez une interface large, demandez-vous quelles classes l'utilisent et lesquelles de ses méthodes sont vraiment nécessaires. Découpez-la en interfaces spécifiques pour simplifier la conception.

Exemple 3 : Refactoring d'une interface lourde

Imaginons que vous travaillez avec une interface existante qui est trop large, et vous voulez la refactorer pour respecter ISP. Prenons une interface Machine qui force toutes les machines à avoir des comportements différents :

public interface Machine {
    void demarrer();
    void arreter();
    void nettoyer();
}

Si vous implémentez une classe RobotCuisine, vous n'avez pas forcément besoin de la méthode nettoyer, qui pourrait être propre à une machine spécifique. Pour respecter ISP, je vous recommande de scinder cette interface en plusieurs interfaces plus petites et spécifiques :

public interface Demarrage {
    void demarrer();
    void arreter();
}

public interface Nettoyage {
    void nettoyer();
}

Ensuite, RobotCuisine n’implémentera que les méthodes nécessaires :

public class RobotCuisine implements Demarrage {
    @Override
    public void demarrer() {
        System.out.println("Le robot démarre.");
    }

    @Override
    public void arreter() {
        System.out.println("Le robot s'arrête.");
    }
}

Si vous avez une autre classe qui a besoin de la méthode nettoyer, comme une classe LaveVaisselle, elle peut implémenter l'interface Nettoyage :

public class LaveVaisselle implements Demarrage, Nettoyage {
    @Override
    public void demarrer() {
        System.out.println("Le lave-vaisselle démarre.");
    }

    @Override
    public void arreter() {
        System.out.println("Le lave-vaisselle s'arrête.");
    }

    @Override
    public void nettoyer() {
        System.out.println("Le lave-vaisselle nettoie.");
    }
}

Cette séparation rend le code plus flexible, plus modulaire et plus simple à tester.


Vous voulez acquérir le réflexe de découper une interface au bon endroit, sans y penser ?

Sentir qu'une interface est trop large et savoir exactement où la scinder, ça ne s'apprend pas dans un article : ça se travaille sur votre vrai code. En mentoring 1:1, je relis vos abstractions avec vous, on refactore ensemble vos contrats les plus douloureux, et vous repartez avec le coup d'oeil qui vous fait éviter ces fourre-tout dès la conception.

Les erreurs courantes à éviter

Même si le principe de séparation d’interface semble simple, il est courant de tomber dans certains pièges lors de son application. Voici quelques erreurs fréquentes à éviter pour que votre code reste propre, modulaire et facile à maintenir.

1. Créer trop d'interfaces inutiles

L'erreur la plus fréquente, c'est de sur-appliquer ISP en créant des dizaines d'interfaces ultra-spécifiques. Le système devient fragmenté, illisible, et le pattern d'origine se perd. Cherchez l'équilibre : assez petites pour ne contenir que ce dont une classe a besoin, mais pas au point de devenir redondantes ou inutilisables.

Exemple à éviter :

public interface Travailler {
    void ecrireCode();
}

public interface ManagerProjet {
    void organiserReunion();
}

public interface SalarieEntreprise {
    void calculerSalaire();
}

Cet exemple montre des interfaces beaucoup trop spécifiques. Parfois, il est plus judicieux de regrouper certaines responsabilités sous une seule interface quand cela fait sens.

Tip : regrouper des comportements similaires sous une même interface quand le métier l'autorise garde le code simple et évite la sur-segmentation. C'est un équilibre que j'ai appris à trouver chez Agirc-Arrco et Canal+ : ni fragmentation excessive, ni interface "dieu".

2. Concevoir des interfaces trop larges

L'autre extrême, c'est l'interface fourre-tout, l'inverse exact d'ISP. Une interface qui regroupe trop de responsabilités force les classes à implémenter des méthodes dont elles n'ont pas besoin. Couplage et complexité explosent pour rien.

Exemple à éviter :

public interface ServiceClient {
    void traiterCommande();
    void envoyerFacture();
    void gererReclamations();
    void realiserAudit();
}

Ici, une interface ServiceClient regroupe des fonctionnalités très différentes, forçant les classes qui l’implémentent à prendre en charge des tâches qui n'ont rien à voir entre elles.

3. Ne pas reconnaître les symptômes d’une interface trop lourde

Si vous vous rendez compte que vous devez fréquemment passer des paramètres null ou lever des exceptions pour des méthodes non implémentées, c’est un signe clair que votre interface est trop large. Une bonne interface ne devrait jamais forcer une classe à contourner des méthodes dont elle n’a pas besoin.

Exemple :

public class ServiceLivraison implements ServiceClient {
    @Override
    public void traiterCommande() {
        // Code de traitement de commande
    }

    @Override
    public void envoyerFacture() {
        // Cette méthode n'est pas pertinente pour ServiceLivraison.
        throw new UnsupportedOperationException("Méthode non supportée.");
    }

    @Override
    public void gererReclamations() {
        // Code de gestion des réclamations
    }

    @Override
    public void realiserAudit() {
        // Non pertinent pour ServiceLivraison.
        throw new UnsupportedOperationException("Méthode non supportée.");
    }
}

Dans cet exemple, la classe ServiceLivraison est obligée de gérer des méthodes inutiles en lançant des exceptions. Cela viole complètement ISP et rend le code plus difficile à maintenir.

4. Ne pas penser aux changements futurs

Concevoir des interfaces sans anticiper les évolutions du système est un piège classique. Ajouter une méthode à une interface large impose une modification à toutes les classes qui l'implémentent, y compris celles qui n'ont aucun usage de la nouvelle fonctionnalité. Des interfaces ciblées dès le départ évitent ce problème et rendent le code plus évolutif.

Tip : Lorsque vous ajoutez une nouvelle fonctionnalité, vérifiez si elle doit vraiment être implémentée par toutes les classes existantes. Si ce n'est pas le cas, il est probablement préférable de créer une nouvelle interface.


Bien découper une interface n'est qu'une pratique sur 100

Appliquer ISP proprement est un réflexe parmi tous ceux qui séparent un dev senior du reste. Le Craft Bundle réunit les 100 pratiques que j'applique au quotidien pour concevoir des abstractions justes et un code qui reste maintenable. Ce sont celles que l'IA ne vous apprendra jamais, parce qu'elle ne les a jamais vues tenir en production.


FAQ sur le principe ISP (Interface Segregation Principle)

1. Qu’est-ce que le principe de séparation d’interface (ISP) en quelques mots ?

Le principe de séparation d’interface (ISP) stipule qu’une interface ne doit pas forcer une classe à implémenter des méthodes dont elle n’a pas besoin. Cela signifie que chaque interface doit être spécialisée et conçue pour répondre à un rôle ou une responsabilité précise, plutôt que d’essayer de tout englober.

2. Pourquoi est-ce important de respecter ISP ?

Respecter ISP améliore la maintenabilité, la flexibilité et la réutilisabilité de votre code. Si une interface est trop large, cela entraîne un couplage élevé entre les classes, rendant le code difficile à modifier et à tester. Avec ISP, chaque classe n’implémente que les fonctionnalités dont elle a besoin, ce qui rend votre système plus modulaire.

3. Comment puis-je savoir qu’une interface est trop large ?

Si vous vous retrouvez à implémenter des méthodes dont votre classe n’a pas besoin ou à lever des exceptions du type UnsupportedOperationException, c’est un signe que votre interface est trop large. De plus, si vous êtes obligé de modifier plusieurs classes à chaque fois que vous ajoutez ou modifiez une méthode dans une interface, cela signifie probablement que cette interface regroupe trop de responsabilités.

4. Combien d’interfaces devrais-je créer ?

Il n’y a pas de nombre exact d’interfaces à créer. Le principe est de diviser les interfaces de manière à ce qu’elles soient aussi spécifiques que nécessaire, mais pas au point de devenir inutiles ou trop fragmentées. Le but est d’atteindre un équilibre entre la spécificité et la simplicité.

5. Quelles sont les différences entre ISP et le principe de responsabilité unique (SRP) ?

ISP et SRP se ressemblent dans l’idée de limiter les responsabilités, mais ils s’appliquent à différents niveaux. SRP concerne le fait qu’une classe ne devrait avoir qu’une seule raison de changer, tandis qu’ISP se focalise sur le fait qu’une interface ne devrait contenir que des méthodes pertinentes pour les classes qui l’implémentent. Les deux principes se complètent : des interfaces bien segmentées facilitent l’application du Dependency Inversion Principle, car les modules de haut niveau peuvent dépendre d’abstractions précises plutôt que de contrats fourre-tout.

6. Quel est le lien entre ISP et les autres principes SOLID ?

ISP fait partie des cinq principes SOLID, qui sont conçus pour améliorer la qualité du code orienté objet. Il est étroitement lié au principe SRP, car ils cherchent tous deux à réduire la complexité du code en limitant les responsabilités. L’ISP aide aussi à réduire le couplage, ce qui est un objectif clé du principe de l’inversion des dépendances (DIP).

7. Quels sont les avantages d’ISP pour les tests unitaires ?

En séparant les interfaces selon leurs responsabilités spécifiques, vous simplifiez les tests unitaires. Comme les classes n’implémentent que ce dont elles ont besoin, il est plus facile de simuler des comportements (via des mocks ou des stubs) et de tester chaque fonctionnalité de manière isolée, sans se soucier des autres méthodes inutiles.

8. Comment appliquer ISP dans des projets existants sans tout refactorer ?

Vous pouvez appliquer ISP progressivement dans un projet existant. Commencez par identifier les interfaces trop larges qui causent le plus de problèmes, puis divisez-les en interfaces plus spécifiques. Vous pouvez également adopter ISP lors de la création de nouvelles fonctionnalités, sans forcément refactorer l’intégralité du projet d’un coup.

9. Peut-on respecter ISP dans des langages autres que Java ?

Oui, ISP est un principe général du développement orienté objet, donc vous pouvez l’appliquer dans la plupart des langages orientés objet, comme C#, Python, ou même C++. Bien que l’implémentation diffère légèrement selon le langage, l’idée reste la même : garder les interfaces petites et spécifiques.


En suivant ces conseils et en évitant les erreurs courantes, vous pourrez tirer le meilleur parti du principe de séparation d'interface (ISP) et écrire un code modulaire, maintenable et facile à tester.


Ressource gratuite : 10 signaux que votre équipe tech est en danger

10 signaux d'alarme pour identifier les problèmes systémiques cachés dans votre équipe avant qu'ils deviennent critiques. Auto-diagnostic inclus : 5 minutes pour savoir où vous en êtes.


Ecris par Kamanga

Expert IT avec 25 ans d'expérience en développement logiciel, diplômé EPITECH et MBA. Spécialisé en software craftsmanship, gestion du changement, stratégie, direction des systèmes d'information, coaching et certifié en agilité.