Continuons notre exploration des principes SOLID avec le quatrième d'entre eux : l'interface Segregation Principle ou ISP que Wikipedia résume ainsi :

📚 Ce principe stipule qu'aucun client ne devrait dépendre de méthodes qu'il n'utilise pas. Il faut donc diviser les interfaces volumineuses en plus petites plus spécifiques, de sorte que les clients n'ont accès qu'aux méthodes intéressantes pour eux.

Si vous avez lu les post précédents sur les principes SOLID, notamment celui sur le Single Responsibility Principle (SRP), vous en apercevez peut-être déjà le but : ici aussi, il s'agit de mettre en place un système de couplage faible, pour tendre vers le sacro-saint Low Coupling, High Cohesion via la conception de classes et d'interfaces qui sont cohérentes, modulaires et faciles à maintenir.

Compréhension du principe

Une fois n'est pas coutume, je vais continuer à imaginer que je suis en train de développer un futur banger intemporel dans le mileu des jeux vidéo indépendants. Cette fois-ci, j'ai abandonné les RPG pour me lancer dans un jeu de course (bourré de micro-transactions qui vont me rendre richissime 🤞)

car

C'est de tout beauté 🤌

J'ai commencé par modéliser mon domaine avec une interface Vehicle qui définit des méthodes pour tous les types de véhicules, tels que startEngine(), stopEngine(), accelerate() ou encore openDoors().

J'ai ensuite continué par l'implémentation de deux types de véhicules : Car et Motorcycle (je n'ai jamais dit que ça serait un jeu original...)

Bien que les voitures et les motos partagent certaines fonctionnalités, comme startEngine() et stopEngine(), une moto n'a pas de portes, ce qui signifie que la méthode openDoors() n'est pas applicable pour elle :

interface Vehicle {
  startEngine(): void;
  stopEngine(): void;
  accelerate(): void;
  openDoors(): void;
}
 
class Car implements Vehicle {
  startEngine() {
    // 🏎️💥
  }
 
  stopEngine() {
    // 🏎️✋
  }
 
  accelerate() {
    // 🏎️📈
  }
 
  openDoors() {
    // 🔑🚪
  }
}
 
class Motorcycle implements Vehicle {
  startEngine() {
    // 🏍️💥
  }
 
  stopEngine() {
    // 🏍️✋
  }
 
  accelerate() {
    // 🏍️📈
  }
 
  openDoors() {
    // ⁉️ How doe ⁉️
  }
}

La classe Motorcycle est forcée d'implémenter une méthode openDoors() qui n'a aucun sens pour elle. C'est une violation du principe ISP (🚨👮✋) : la classe dépend d'une méthode qu'elle n'utilise pas.

Pour résoudre ce problème, nous allons devoir diviser l'interface Vehicle en interfaces plus petites et spécifiques. Mais avant ça, quel sont les problèmes que posent cette violation de l'ISP ?

Pourquoi ce code est problématique

Complexité accrue : Une violation de l'ISP peut entraîner une complexité accrue dans le code, car les classes doivent prendre en charge des fonctionnalités dont elles n'ont pas besoin. Cela rend le code plus difficile à comprendre et à maintenir. Dans le cadre d'un langage typé comme le Typescript, cela peut obliger les méthodes inutiles à throw une erreur de type method not implemented (sic).

Fragilité du code : Lorsque des classes dépendent d'interfaces avec des fonctionnalités non pertinentes, toute modification de ces interfaces peut affecter involontairement les classes qui les utilisent. Cela rend le code plus fragile et augmente le risque d'introduire des bugs lors de la maintenance ou de l'évolution du système.

Dépendances inutiles : Cette violation introduit des dépendances inutiles entre les composants du système. Cela peut rendre le système plus difficile à tester, à debuguer et à refactoriser, car les changements dans une partie du système peuvent avoir des répercussions inattendues sur d'autres parties.

Difficulté de réutilisation : Des interfaces monolithiques ou surchargées rendent plus difficile la réutilisation des classes dans d'autres contextes. Les classes qui fournissent des fonctionnalités non pertinentes peuvent être difficiles à intégrer dans d'autres parties du système, car elles ont des dépendances excessives.

Application du principe ISP

Appliquons le principe ISP à notre exemple. Nous allons diviser l'interface Vehicle deux interfaces spécifiques : Motorized et DoorControllable.

interface Motorized {
  startEngine(): void;
  stopEngine(): void;
  accelerate(): void;
}
 
interface DoorControllable {
  openDoors(): void;
}
 
class Car implements Motorized, DoorControllable {
  startEngine() {
    // 🏎️💥
  }
 
  stopEngine() {
    // 🏎️✋
  }
 
  accelerate() {
    // 🏎️📈
  }
 
  openDoors() {
    // 🔑🚪
  }
}
 
class Motorcycle implements Motorized {
  startEngine() {
    // 🏍️💥
  }
 
  stopEngine() {
    // 🏍️✋
  }
 
  accelerate() {
    // 🏍️📈
  }
}

Désormais chaque classe n'implémente que les méthodes qui lui sont pertinentes, conformément au principe ISP. La classe Motorcycle n'a pas besoin de fournir une implémentation pour la méthode openDoors(), ce qui la rend plus cohérente et maintenable.

car-motorbike

Oui, bon, en vérité le jeu ressemble à ça pour le moment 😅🥲

Intérêts de l'ISP

Ce principe encourage la conception d'interfaces spécifiques et cohérentes, ce qui conduit à des systèmes modulaires et faciles à maintenir. Cela comporte un certain nombre d'avantages, parmi lesquels :

La facilité de maintenance : En veillant à ce que les clients ne dépendent que des méthodes qu'ils utilisent, le code est plus facile à maintenir et à comprendre.

Flexibilité : Le principe de ségrégation des interfaces encourage la conception d'interfaces ciblées et modulaires, ce qui permet d'obtenir un code plus polyvalent, capable de s'adapter à l'évolution des besoins.

Testabilité : En adhérant à l'ISP, vous pouvez simplifier vos suites de tests, car les clients n'auront à tester que les méthodes qu'ils utilisent réellement.

Réutilisation : L'ISP favorise la création d'interfaces restreintes et ciblées qui peuvent être facilement réutilisées dans différents contextes.

Conclusion

Lorsque vous concevez vos interfaces, vous devez viser à la fois une forte cohésion et un faible couplage. Cela signifie que vos interfaces doivent avoir un objectif clair et unique, et qu'elles ne doivent exposer que les méthodes pertinentes pour cet objectif. Vous devez également éviter d'hériter d'interfaces génériques de grande taille, car cela créerait des dépendances inutiles et violerait l'ISP. Au lieu de cela, vous devez favoriser la composition plutôt que l'héritage, et utiliser plusieurs interfaces plus petites et spécifiques pour définir le comportement de vos classes.