Architecture logicielle & Développement

Le Principe de Ségrégation des Interfaces | 0 vote(s)

Tags: ,

Il reste encore certains principes de conception orientée objet que nous n’avons pas approché – le principe de ségrégation des interfaces (ISP, interface segregation principle) est l’un d’eux.

A première vue, ce principe ressemble au principe de responsabilité unique (SRP, single responsability principle), mais cette ressemblance n’est que partielle. Alors que le SRP nous dit qu’une classe ne doit avoir qu’une seule raison de changer (ce qui nous oblige à partager les responsabilités dans différentes classes), l’ISP gère quand à lui des problèmes liés aux dépendances du code client. Les effets de la non-application de ce principe sont donc différents.

Pourquoi ?

Considérons le cas suivant : Un capteur est avertit à intervalle régulier de l’heure. En fonction de celle-ci, il lui est possible d’effectuer une action particulière (par exemple, une acquisition de données).

Une solution qui semble répondre à ce problème est de faire hériter la classe Capteur d’une classe Observeur permettant de se lier à l’horloge ou à un autre générateur d’événements, et de dériver ce Capteur une classe CapteurSynchrone qui traite le message généré par l’objet Horloge. Le diagramme UML[1] ci-dessous montre les différentes relations entre ces classes.

Diagramme de classe UML

Bien que grossièrement satisfaisante du point de vue du code et respectant le SRP, ce code pose quand même un problème. Supposons maintenant que je souhaite implémenter une classe CapteurAutonome, qui ne dépend d’aucune information extérieure et qui gère lui-même la façon dont il récupère ses informations.

Dans la conception exprimée ci-dessus, je devrais logiquement dériver ce CapteurAutonome de Capteur. C’est là que le bat blesse : pourquoi mon capteur autonome devrait-il dépendre de la classe Observeur, alors que celle-ci ne lui est d’aucune utilité ?

Vers une plus grande indépendance des interfaces

Il existe plusieurs méthodes qui permettent de limiter ce problème. La première est la plus simple à mettre en œuvre, même si elle s’accompagne quelques fois de subtilités : l’héritage multiple.

Dans l’exemple précédent, je peux refactoriser CapteurSynchrone afin qu’il dérive de Capteur et de Observeur (ce qui me permet en outre de spécialiser cette dernière en une classe ObserveurHorloge pour gérer les messages de l’horloge uniquement). La classe Capteur devient indépendante, ce qui me permet de la dériver en CapteurAutonome.

Cependant, tous les langages ne permettent pas l’héritage multiple, et la technique en elle-même est quelques fois difficile à mettre en œuvre[2].

Heureusement, il est tout à fait possible de mettre en place d’autres solutions pour palier à ce problème. L’utilisation judicieuse du patron de conception Adapteur, par exemple, permet de composer le CapteurSynchrone dans une classe adapteur qui hérite de ObserveurHorloge. Cette classe peut alors actionner le capteur synchrone lorsqu’elle reçoit de nouvelles informations en provenance de l’horloge.

A propos des arguments de fonctions

Supposons que je travaille sur l’interface graphique d’un système de gestion de compte bancaire. Le SRP m’a dicté que la partie de l’interface graphique qui permettait de gérer les dépôts (GuiDepot) et celle qui permettait de gérer les virements (GuiVirement) étaient séparées, mais j’ai aussi un objet Gui qui dérive de ces deux interface. Je dois maintenant écrire une fonction qui accède à celle ci.

Robert C. Martin[3] pose alors la question suivante : dois écrire une fonction g(Gui&) (forme monadique) ou g(GuiDepot&, GuiVirement&) (forme polyadique) ?

Si j’utilise la forme polyadique, le code écrit semble être étrange. Après tout, je sais que mon interface graphique est unique, et mon instance de GuiDepot ainsi que mon instance de GuiVirement sont toutes les deux contenues dans mon instance de la classe Gui. Pour utiliser g, je vais donc écrire :

Gui mon_interface;
// ...
g(mon_interface, mon_interface);

Ce qui semble étrange à première vue.

En utilisant la forme monadique, le code est plus simple :

g(mon_interface);

Cependant, la simplicité du code cache un problème de plus grande importance, car g() est maintenant dépendante de toutes les interfaces qui composent Gui, y compris celles dont elle n’a aucune utilité. Par conséquent, on préfèrera la forme polyadique, malgré son apparente complexité et son étonnante signature.

Conclusion

Le principe de responsabilité unique est une règle de construction d’interface dont le but est d’éviter la création de classes regroupant plusieurs responsabilités. Le principe de ségrégation des interfaces, quant à lui, est une règle qui décrit la façon dont les liens de dépendances des interfaces doivent se faire – ou plus exactement, elle définit les liens qui ne doivent pas se faire.

Une classe ou une fonction cliente ne doit pas dépendre d’interfaces dont elle n’a pas l’utilité

Notes

[1] généré grâce au logiciel open-source StarUML

[2] sans parler des résistances humaines que la simple évocation des mots "héritage multiple" peut causer...

[3] l’exemple cité est directement tiré de son article Interface Segregation Principle, disponible en PDF ici

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

Aucun commentaire pour le moment.

Ajouter un commentaire

Si votre navigateur est compatible, vous pouvez vous aider de la barre d'outils placée au-dessus de la zone de saisie pour enrichir vos commentaires.