Vous n'avez (probablement) pas besoin de cette interface

Et surtout pas de cette implémentation qui s'appelle « NomDeMonInterfaceImpl »

Dans certains langages de programmation1, on a beaucoup tendance à créer des interfaces à la pelle sans trop réfléchir, puis à leur créer une et une seule implémentation. Ça fait qu’on se retrouve avec un code qui contient énormément d’interfaces InterfaceName et d’implémentations nommées InterfaceNameImpl.

Et c’est un problème.

Pourquoi c’est un problème ?

  • Parce que c’est signe d’un code qui n’est pas conçu, mais qui est développé en appliquant sans réfléchir des « bonnes pratiques » dont on ne sait ni d’où elles sortent, ni si elles sont applicables au cas développé.
  • Parce que ça complexifie la navigation dans le code.
  • Parce que ça n’apporte souvent rien d’avoir extrait cette interface : ni une meilleure lisibilité, ni une facilité de maintenance, ni une facilité d’évolution dans les faits2.
  • Parce que ça peut même complexifier la maintenance en ajoutant des modifications non-nécessaires sur l’interface et sur l’implémentation si l’interface doit changer, contre une seule modification s’il n’y a pas cette interface.
  • Parce que ça éloigne la documentation (souvent écrite dans l’interface) de son implémentation (écrite dans la classe).

Ainsi donc :

Vous qui développez, ne créez des interfaces dans votre code que si elles sont nécessaires à son bon fonctionnement.

Et là on me demandera :

Dis-moi, renard, comment sais-je qu’une interface est nécessaire à mon code ?

En vérité c’est assez simple. Deux cas vous indiquent que vous devriez avoir une interface :

  1. Vous développez une bibliothèque : mettez toute l’API publique dans des interfaces. Ça permet une excellente séparation de ce qui est public (et utilisable par les tiers) de ce qui est du détail d’implémentation et « public » pour raisons techniques. Vous pouvez même distribuer ces interfaces dans un paquet séparé si c’est pertinent dans votre cas.
  2. Vous savez déjà, lors de votre conception, que vous avez plusieurs implémentations distinctes à réaliser – maintenant ou dans un futur proche et non-hypothétique.

Un autre cas vous indique que vous ne devriez pas créer d’interface : si vous vous apprêtez à en créer une seule implémentation, surtout si cette implémentation est tellement peu spécifique que vous ne lui avez pas trouvé meilleur nom que celui de l’interface suivi de Impl.

Le reste est à gérer au cas par cas, mais n’oubliez pas que les IDE modernes permettent très simplement d’extraire une interface du code existant.


Quant à l’argument très souvent lu qui prétend que « programmer par interfaces permet très facilement de changer une implémentation, de remplacer une ancienne implémentation par une nouvelle sans changer le reste du code ou bien d’adapter le code aux nouveaux besoin », je voudrais vous livrer une expérience personnelle. Ça fait maintenant plus de douze ans que je développe en Java de façon continue et professionnelle, et j’ai travaillé sur des projets variés, du gros projet e-commerce au petit site de présentation de produits, en passant par des applications Android de toutes tailles, un moteur de résolution de règles, divers progiciels métiers avec quantité de règles absconses, des projets dont le premier code a vu le jour entre 2000 et 2022…

Dans tout ça, savez-vous combien de fois j’ai pu appliquer ledit argument, c’est-à-dire remplacer une implémentation par une autre sans avoir besoin de réaliser des changements massifs d’interface ?

Absolument jamais.

Cette règle est un reliquat d’anciennes pratiques de développement3 sur de gros projets monolithiques. Elle n’a plus de raison d’être aujourd’hui.


  1. Oui, Java, c’est à toi que je pense !
  2. Je parle de faits et uniquement de faits ici, pas de théorie vue en cours ou dans des bouquins.
  3. Quand un ingénieur écrivait des interfaces et laissait les implémentations à des batteries de techniciens. Ça a existé dans de très gros projets de très grosses entreprises, genre IBM. Les projets de taille juste inférieure gérés par des entreprises un peu moins ultra-rigoureuses ont plutôt fini en tas de spaghettis – ce qui est largement pire qu’un surplus d’interfaces, soit dit en passant.


Merci de m’avoir lu.

La prochaine fois, on parlera des accesseurs et mutateurs, et de cette engeance de Satan que sont les getters/setters autogénérés.


Icône : création personnelle, CC BY 4.0 si tant est qu’on puisse mettre une licence sur un truc aussi trivial.

12 commentaires

L’interfacite est surtout une maladie qu’on apprend à l’école avec pour principal argument qu’il faut faire comme ça parce que c’est mieux.

Bien sûr quand un élève un peu curieux en demande plus, l’argument du "changer l’implémentation à la volée" arrive. Mais même si j’ai moins d’XP que toi, je confirme aussi : je n’ai jamais eu à remplacer une implémentation. Il m’est arrivé d’avoir la possibilité d’utiliser plusieurs implémentations en parallèle ou bien d’utiliser le polymorphisme pour qu’un algorithme unique soit pertinent pour plusieurs hiérarchie de classes mais je n’ai jamais eu à remplacer.

En vrai la "programmation par interface" à la mode "école" (je fais d’abord 30 interfaces avant de me rendre compte qu’à la fin j’utilise juste des classes) n’a qu’un seul et unique intérêt : à la fin tu as des supers diagrammes UML. Et à l’école, les diagramme UML c’est ce que la prof attend de toi.

Dans la vie de tous les jours, une interface n’a en effet de sens que si tu as besoin d’avoir tout de suite ou dans un futur très proche d’une abstraction pour faire un algorithme génétique. Ca permet à la fois de découpler mais surtout de mieux documenter. Notamment quand tu dois décrire un how to.

D’ailleurs les getters et setters sont du même acabit. On a fait d’une particularité du langage java (les niveaux d’accès) une norme puis on a dit "met les attributs en private et fait des accesseurs/mutateurs" et surtout tu as sonar qui te fait bien chier si tu fais pas ça.

C’est tellement mieux de voir ce que C# fait avec ses attributs.

L’interfacite est surtout une maladie qu’on apprend à l’école avec pour principal argument qu’il faut faire comme ça parce que c’est mieux.

artragis

Oh, la maladie du « On fait comme ça parce que c’est mieux » (sans autre argument) est aussi très présente en entreprise, même si elle concerne assez peu l’interfacite (au moins sur les projets récents et les équipes à jour).

J’ai eu des discussions très intéressantes juste en enchainant les « Mais pourquoi… » dans mes différentes boites. Très souvent ça conduit à un moment où à un autre à des réponses du type « C’est historique, on en a plus besoin maintenant / on peut faire autrement ». N’hésitez pas à faire de même !

Dans la pratique, si tu bosses pour des entreprises qui vont mettre en concurrence des implémentations, tu n’as pas d’autres choix que d’utiliser des interfaces. Ce n’est PAS le cas généralement, très loin de là.

Dans mon taf actuel, on utilise le principe d’interfaces privées. Oui vous m’avez bien lu.
Je suis extrêmement sceptique face à cette pratique.

PS: Je n’ai pas le sentiment que l’on m’ait martelé à l’école d’utiliser des interfaces mais force est de constater que ça marche mieux que le modèle d’héritage de l’OO. Le système de Traits est également très intéressant et en vogue.

+1 -0

Dans la pratique, si tu bosses pour des entreprises qui vont mettre en concurrence des implémentations, tu n’as pas d’autres choix que d’utiliser des interfaces. Ce n’est PAS le cas généralement, très loin de là.

ache

J’ai du mal à voir un cas d’usage concret (à moins que tu ne parles des API/ABI publiques ?) mais sinon ça ressemble à un usage légitime des interfaces.

PS: Je n’ai pas le sentiment que l’on m’ait martelé à l’école d’utiliser des interfaces mais force est de constater que ça marche mieux que le modèle d’héritage de l’OO. Le système de Traits est également très intéressant et en vogue.

ache

Je ne suis pas sûr de comprendre ton propos sur les interfaces par rapport à l’héritage, si tu peux détailler ?

Sinon, je remercie beaucoup les frameworks très utilisés actuellement dans le monde de Java qui font de l’injection de dépendances, et donc tendent naturellement vers un usage de la composition au lieu des longues chaines d’héritages qu’on pouvait facilement trouver avant.

J’ai du mal à voir un cas d’usage concret (à moins que tu ne parles des API/ABI publiques ?) mais sinon ça ressemble à un usage légitime des interfaces.

Je ne pense pas que ça soit des API publiques mais lorsque je bossais pour l’embarquée pour des constructeurs automobiles les interfaces étaient fixées afin que l’on puisse comparer le code avec ceux d’autres projets (ou même de l’ancienne version). Dans le but affiché de pouvoir remplacer l’un par l’autre si jamais on devait changer des spécifications hardware (gagner de l’argent …). La norme du secteur est AUTOSAR et non elle ne définie pas d’API publique pour tout.

Je ne suis pas sûr de comprendre ton propos sur les interfaces par rapport à l’héritage, si tu peux détailler ?

L’héritage utilisée pour créer des codes génériques n’est pas aussi efficace que le système d’interface. Il faut se restreindre à l’utiliser pour factoriser du code quand elle a plus de sens qu’utiliser une composition.

+1 -0

Je ne pense pas que ça soit des API publiques mais lorsque je bossais pour l’embarquée pour des constructeurs automobiles les interfaces étaient fixées afin que l’on puisse comparer le code avec ceux d’autres projets (ou même de l’ancienne version). Dans le but affiché de pouvoir remplacer l’un par l’autre si jamais on devait changer des spécifications hardware (gagner de l’argent …). La norme du secteur est AUTOSAR et non elle ne définie pas d’API publique pour tout.

ache

C’est une API « publique » dans le sens où elle est définie par un tiers au programme (même si elle ne sert pas explicitement à interfacer le programme avec autre chose). Le fait d’implémenter ça via des interfaces me parait logique, tout comme ça serait le cas pour n’importe quelle norme structurante.

L’héritage utilisée pour créer des codes génériques n’est pas aussi efficace que le système d’interface. Il faut se restreindre à l’utiliser pour factoriser du code quand elle a plus de sens qu’utiliser une composition.

ache

Là, désolé, mais je ne comprends toujours pas. Pour moi on compare des choux et des carottes, l’héritage et les interfaces n’ayant pas du tout (du moins dans les langages que je connais) les mêmes propriétés ou les mêmes buts. L’interface est là pour définir, eh bien, une interface, une façon de communiquer avec la classe, quand l’héritage est plutôt là pour factoriser du code. On pourrait pinailler sur les implémentations par défaut dans les interfaces en Java, qui est ce qui se rapproche le plus des traits qu’on trouve dans d’autres langages, mais là le résultat s’approche plus d’un héritage multiple en plus sûr que d’une interface au sens de la définition d’une façon de communiquer avec la classe.

Je suis d’accord que trop de devs abusent des interfaces (et est un des plus gros mensonges qu’on raconte aux devs). Mais résumer leurs nécessités à ces deux seuls cas me semble téméraire.

De mon point de vue, il manque plusieurs cas où une interface est indispensable même quand on sait qu’il n’y aura qu’une seule implémentation (personnellement, je n’ai que rarement croisé des devs qui me parlaient d’« au cas où il y aurait une seconde implémentation», tout au plus le cargo cult chère à toute la communauté dev…) :

Quand une classe dépend d’un service qui n’est pas de ma responsabilité (par exemple, développé par une autre personne) Ça casse la dépendance et me permet d’avancer (je peux faire un double en attendant la vraie implémentation). Ça va aussi m’aider à définir le contrat de ce dont j’ai besoin

Pour des questions de tests : ne pas dépendre d’implémentation autre que celle qu’on veut tester (et encore) simplifie énormément l’écriture des tests.
Je vois difficilement comment écrire des tests unitaires si je ne peux pas stubber/mocker/faker les dépendances. Je vois mal comment maintenir efficacement un projet sur du long terme sans tests unitaires robustes (oui, Mockito rend les tests fragiles)

On sait aujourd’hui de façon empirique qu’un monolithe ne tiendra pas dans le temps (trop difficile de monter en charge, scaler, maintenir, etc.). Il faudra forcément passer par au moins un monolithe modulaire, voire des micro-apps/micro-services. Et, c’est tout de même plus simple quand le design et l’archi le permettent déjà (hexagonal, bounded context, hive, etc.) et qu’on ne doit passer par la phase de rework fastidieuse même si les outils de refacto sont devenus très efficaces.

Sans compter, tous les autres cas plus ou moins philosophiques du couplage, besoin d’en connaître, programmation par capacité, etc. et autres contraintes : MVP, agile, multi-team, etc. où on ne doit pas dépendre des implémentations, si on veut être efficace.

Dernière précision : je ne suis pas en train de dire que les interfaces sont une silver bullet

[…] quand l’héritage est plutôt là pour factoriser du code

Dans des langages comme C++, on présente l’héritage (et le polymorphisme associé) comme une méthode efficace pour avoir une architecture générique. Ce n’est pas aussi efficace que l’utilisation de classes virtuelles pures (interface de Java je crois, je ne code pas en Java ^^").

On est d’accord sur le fond je pense, mais j’ai l’impression que vous, on vous a rabâché d’utiliser des interfaces alors que nous, on nous a rabâché d’utiliser des classes et l’héritage.

+1 -0

De mon point de vue, il manque plusieurs cas où une interface est indispensable même quand on sait qu’il n’y aura qu’une seule implémentation (personnellement, je n’ai que rarement croisé des devs qui me parlaient d’« au cas où il y aurait une seconde implémentation», tout au plus le cargo cult chère à toute la communauté dev…) :

Mumbles

Note que le billet ne prétends absolument pas être complet sur les cas d’utilisation des interfaces – et ça n’a jamais été son but. Je liste surtout deux cas ou elles doivent être créées, et un où elles ne doivent pas exister. La « seconde implémentation » c’est un argument que j’ai trop souvent entendu, en général en bout de chaine de « pourquoi ? » – et clairement, c’était du culte du cargo à chaque fois.

On sait aujourd’hui de façon empirique qu’un monolithe ne tiendra pas dans le temps (trop difficile de monter en charge, scaler, maintenir, etc.). Il faudra forcément passer par au moins un monolithe modulaire, voire des micro-apps/micro-services. Et, c’est tout de même plus simple quand le design et l’archi le permettent déjà (hexagonal, bounded context, hive, etc.) et qu’on ne doit passer par la phase de rework fastidieuse même si les outils de refacto sont devenus très efficaces.

Mumbles

L’exemple est intéressant parce que jusqu’à il y a un an, j’avais pour mission d’essayer d’apporter de la modernité dans la gestion d’un énorme projet monolithique antique (le genre qui a utilisé des JSP quand c’était audacieux de nouveauté). Ce projet était découpé en modules, matérialisés par des packages, qui devaient être le plus indépendant possibles. Une des hypothèses de travail était d’en faire des vrais modules Maven séparés, pour pouvoir les builder indépendamment et en refiler certains à d’autres équipes.

On a jamais pu, parce qu’il y avait des centaines de dépendances cycliques entre modules de type A -> B -> A, des milliers de type A -> B -> C -> A et les boucles d’ordre supérieur n’étaient même pas calculables en un temps raisonnable. Le truc n’était qu’un immense tas de spaghettis où tout dépendait de tout.

Pourtant, chaque classe avait son interface séparée. Ça rejoint ce point :

Sans compter, tous les autres cas plus ou moins philosophiques du couplage, besoin d’en connaître, programmation par capacité, etc. et autres contraintes : MVP, agile, multi-team, etc. où on ne doit pas dépendre des implémentations, si on veut être efficace.

Mumbles

Je suis plutôt d’accord avec ça, mon propos c’est que ces interfaces conceptuelles n’ont souvent pas besoin d’être matérialisées par des interface Xxxx dans le code. Et inversement, ça n’est pas parce que tu les matérialise que ça améliore ces points de façon magique (y’en a qui ont essayé, ils ont eut des problèmes).

On est d’accord sur le fond je pense, mais j’ai l’impression que vous, on vous a rabâché d’utiliser des interfaces alors que nous, on nous a rabâché d’utiliser des classes et l’héritage.

ache

Pour les classes en Java on a pas le choix, mais l’héritage à outrance est aussi très enseigné en Java. Par exemple, le réglage par défaut de Sonar sur la règle « profondeur d’héritage trop grande » c’est quand tu as plus de cinq (5 !) niveaux de parents à ta classe.

 programmer par interfaces permet très facilement de changer une implémentation, de remplacer une ancienne implémentation par une nouvelle sans changer le reste du code ou bien d’adapter le code aux nouveaux besoin 

je trouve cette argument faux, pourquoi? qu’est qui m’interdit en cas de besoin de changer d’implémentation en réécrivant la class. on veux comparer 2 implémentations, je crée 2 branche sur git.

je partage votre avis, créer une interface (comme tout autre élément) doivent pouvoir ce justifier perso, je crée une interface dans 2 situation : quand j’ai besoin de plusieurs implémentations, ou quand je veux gérer le direction de mes dépendances.

(Je découvre longtemps après)

Autre cas légitime: pour le mocking — cité indirectement par mes VDDs. Et je soupçonne que c’est lui qui est à l’origine de l’insistance pour mettre en place le DIP (et donc toutes ces interfaces à outrance)

Sinon, il y a une situation où avoir héritage/interfaces est un non-sens: en présence d’objets valeurs. En C++ on les reconnaîtra au fait que l’on voudra pouvoir les copier. En Java, cela se retrouvera plus au besoin de comparaison/hashing; cf. Effective Java où Joshua Bloch démonte l’héritage Point -> ColoredPoint.

Autre cas légitime: pour le mocking — cité indirectement par mes VDDs. Et je soupçonne que c’est lui qui est à l’origine de l’insistance pour mettre en place le DIP (et donc toutes ces interfaces à outrance)

100 % d’accord, si les tests utilisent réellement cette possibilité. Je ne compte plus la quantité de projets où il y a une palanquée d’interfaces… et un outil dédié comme Mockito pour gérer les mocks >_< (qui pose d’autres problèmes d’ailleurs, mais ça n’est pas le sujet).

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte