SOLID – I: Interface Segregation

Pour comprendre l’ Interface Segregation. Commençons par la définition de « ségrégation » trouvée dans le Larousse :

« Action de mettre à part quelqu’un, un groupe »

Ou si l’on regarde la définition d’un point de vue technique:

 « Séparation en amas distincts d’un ensemble de corps différents préalablement mélangés. »

Vous adaptez ceci à la programmation et plus spécifiquement aux interfaces, et je vous le donne dans le mile, la ségrégation c’est donc splitter une Interface en 2 (ou plus) si celle-ci prévoit trop de choses. Comment pourrait-on faire trop de choses si l’on suit le premier principe de SOLID ? Suivez le guide!

Pour comprendre ce principe, commençons par un classe native de php: ArrayObject. Lorsque vous étendez cette classe, vous pouvez déjà utiliser votre instance de plein de manière différentes: faire un count, un foreach, le sérialiser et accéder à ses attributs comme un tableau. Jusque là, rien de choquant. Pourtant lorsqu’on y regarde de plus près, cette classe étend quatre interfaces:  IteratorAggregate, ArrayAccess, Serializable et Countable. Que diriez vous de regrouper celles-ci pour n’en faire plus qu’une: ArrayObjectInterface ? Bof… Quel est l’intérêt? Elle ferait sans doute « trop de choses »,  même si, au final, elle n’a qu’une responsabilité: Gérer un objet comme tableau.

Dans ce cas-ci, nous avons fait le « chemin inverse » d’une ségrégation d’interface. Votre job consistera donc à couper une interface en petits morceaux. Un peu comme si monsieur PHP était parti de l’ ArrayObjectInterface pour arriver à IteratorAggregate , ArrayAccess , Serializable et Countable. L’avantage est, bien entendu, de n’étendre que le strict nécessaire. La ségrégation la plus extrême est de ne mettre qu’une seule méthode dans une interface, comme c’est le cas pour Countable par exemple.

 

Mais pourquoi fait-on cela ?

Prenons une interface Contact qui représente une personne qui a plusieurs informations basiques: Nom, prénom, e-mail, téléphone et adresse.

Ajoutons à cela une classe PubSender qui s’occupera d’envoyer des publicités (ou spams) à ces pauvres gens. Une méthode pour l’envoi par sms, l’autre pour les e-mails.

Pour savoir si nous sommes dans le bon, posons nous une première question: Lorsque j’envoie un sms, ai-je vraiment besoin de toutes les méthodes de l’objet Contact ? Ai-je besoin que « mon contact » possède un e-mail, une adresse et un nom de famille?

Biensûr que non! Pour l’envoi d’un sms, vous n’avez besoin que d’un numéro de téléphone et d’un prénom. Le problème est le même pour l’envoi d’e-mail qui n’a besoin que d’un e-mail pour vivre.

Ces méthodes ont donc trop de responsabilités car elle reçoivent de trop gros objets. Il est peut-être temps de diviser notre interface Contact même si elle parait déjà si petite.

 

Ségrégons !

Dans ce cas-ci,  l’idée est de faire en sorte que lorsque l’on envoie un sms, on ne demande qu’un objet qui ne contient que les méthodes que l’on va utiliser. Qu’à cela tienne, nous allons créer une interface Smsable qui représente une personne contactable par sms:

Faisons de même pour les gens qui sont contactables par e-mail.

Mon utilisateur (ou contact) peut donc étendre ces 2 interfaces, et y ajouter d’autres méthodes nécessaires.

Refactorons dès lors notre classe PubSender.

Comme vous pouvez le constater, seule la signature de la méthode change. Nous avons à présent des méthodes qui reçoivent des objets taillés sur-mesure pour la fonctionnalité.

Une chose intéressante avec la ségrégation est que vous pouvez plus facilement réutiliser (décidément LE maitre mot des principes SOLID) les classes. En effet, si les arguments des méthodes sont des interfaces simples, il sera beaucoup plus facile de les intégrer dans un autre projet. C’est aussi moins couteux d’implémenter les 2 méthodes de Smsable plutôt que les 5 méthodes de Contact. Ceci est d’autant plus vrai si 3 des 5 méthodes doivent rester vides car vous ne gérez pas les noms, prénoms et adresses postales. Souvenez-vous de ce que disait Barbara Liskov!

 

Autres avantages

En plus d’avoir des méthodes mieux définies, la ségrégation d’interface apporte aussi un autre avantage : Des mock plus simple pour les tests unitaires.

Fini les grosses classes dont il ne faut mocker qu’une petite partie des méthodes. Mais d’ailleurs, quelle partie ? Quelles méthodes ? Qui n’a jamais été vérifier ce qui est réellement appelé pour savoir ce qui est utilisé (et donc ce qu’il faut mocker)? Cela se passe bien moins souvent lorsque les interfaces sont plus petites, car tout, ou presque, est utilisé.

Utiliser la ségrégation d’interface peut aussi aider à mieux diviser son code (et donc à atteindre/garder la responsabilité unique). En effet, une grosse interface « fourre-tout » est bien plus difficile à maintenir que plusieurs petites qui ont plusieurs avantages:

  • Moins de tests unitaires par classe. En plus d’être petites, on peut penser qu’il y aura aussi moins de méthodes « interconnectées » entre elles.
  • Moins de problèmes de régression car moins de méthodes donc moins de code impacté.

Tout cela donne envie n’est-ce pas?

 

Et si j’envoie un mail ET un sms ?

La partie simple était d’envoyer un e-mail ou un sms. Mais que faire si vous devez envoyer une publicité à votre User sur les 2 supports simultanément (donc via une seule méthode).

Une première solution serait d’abord de faire une nouvelle interface Contactable regroupant les deux interfaces et qui sera implémentée par User.

L’envoi des publicités se fera alors en utilisant cette nouvelle interface

Cette solution est viable. Son principal défaut est que vous vous trouvez avec une nouvelle interface qui ne fait rien d’autre que représenter un ensemble d’autres interfaces. Croisons les doigts pour qu’elle ne grossisse pas trop, sinon vous devrez probablement la ségréguer à nouveau.

Une deuxième solution serait que votre classe User implémente les interfaces de base (Smsable et Emailable) sans passer par une interface intermédiaire. Rien d’extraordinaire pour l’instant.

En écrivant la méthode « send », vous vous dites logiquement que vous aurez besoin de 2 objets, car il y a deux supports: un objet Smsable et un Emailable.

Vous l’avez certainement deviné, comme User étend ces 2 interfaces, on va devoir le passer deux fois à la méthode.

Horreur et stupéfaction! Avant de vous précipiter sur la première solution, mettons les choses au clair.

MultiPubSender veut envoyer une publicité à deux supports différents. Elle n’a clairement pas besoin de savoir si ceux-ci appartiennent à la même personne. Nous n’avons donc aucune raison de changer sa signature, n’est-ce pas? Pourquoi devrait-elle s’adapter à votre objet User?

Oui, le monde est cruel car la même instance de User peut être vu de deux manières différentes (Smsable et Emailable). Mais c’est le problème de User (et le vôtre au passage) pas celui de MultiPubSender! L’inconvénient de cette solution est donc évident: On passe plusieurs fois le même objet à une méthode. Comme on dit chez nous: « ça l’fait pas! ». Par contre, vous laissez une porte ouverte pour le jour où les personnes de contact seront différentes (Open/Closed).

 

Conclusion

La ségrégation d’interface est un chouette principe, si on ne tombe pas dans l’extrême. Si vous poussez le bouchon un peu trop loin, vous n’aurez que des interfaces à une méthode. C’est avec la pratique que vous allez apprendre à bien les utiliser. N’hésitez pas à tester vos connaissances, à vous tromper, refactorer (<> refacturer), re-essayer, etc. Débattez avec vos collègues sur les interfaces à créer ou modifier.

Au début, cela peut sembler compliqué voire inutile pour ceux qui n’utilisent jamais d’interface, mais comme je l’ai déjà dit, elle pourra aussi vous aider à garder un code qui suit les principes de responsabilité unique. Comme quoi, tous les principes SOLID sont liés.

Quant au dernier exemple, un peu tordu mais bien réel, il n’est pas si fréquent que cela. Je suis sûr que chacun trouvera le compromis qui lui convient le mieux : Que ce soit avec une des solutions évoquées ou avec du code supplémentaire comme des adaptateurs, convertisseurs, etc.