Inversion de dépendance : pourquoi tes interfaces rendent ton code illisible

Deezer, salle de réunion, présentation des principes SOLID devant l’équipe web. J’arrive sur le « D » : le Principle d’Inversion de Dépendance. Mes collègues interprètent ça comme : « Une interface partout, comme ça je peux changer d’implémentation sans toucher au reste du code. »

Moi le premier, à mes débuts, je pensais pareil. Chaque classe cachée derrière une interface : class DefaultRepository avec son interface Repository, class GetLicenceUseCase avec son interface GetLicenceUseCaseContract même quand une seule implémentation existait et existerait toujours

Résultat : en théorie un code imbattable en flexibilité mais difficile à lire.

Le clic qui ne mène nulle part

Illustration d'un développeur Android et Kotlin Multiplatform suivant les flèches de navigation entre le fichier d'interface et son implémentation concrète dans le code

Tu ouvres un fichier. Tu vois licenceDataSource.getLicence(userId). Tu cliques sur getLicence pour comprendre ce qui se passe.

Android Studio t’envoie sur l’interface LicenceDataSource. Une signature. Zéro logique. Tu remontes, tu cherches l’implémentation concrète, RemoteLicenceDataSource ou LocalLicenceDataSource selon les cas. Deux clics, un aller-retour, pour lire une ligne de code.

Multiplie ça par des centaines de fichiers dans une codebase, et l’onboarding devient franchement pénible.

Ce que dit vraiment le principe d’inversion de dépendance

Le principe d’inversion de dépendance dit ceci : ta logique métier ne doit jamais dépendre directement d’un détail d’implémentation. Les deux doivent dépendre d’un contrat commun.

Concrètement : ton Repository ne doit pas savoir si les licences viennent d’une API REST ou d’une base SQLite. Il dépend de LicenceDataSource, pas du code qui parle réellement au réseau.

Le principe ne dit pas : crée une interface pour chaque classe. Il dit : protège ta logique métier des couches externes ou celles qui peuvent bouger souvent.

Cette protection sert à gérer une variation ou une frontière. Ce n’est pas un rituel à appliquer par défaut.

La règle d’inversion de dépendance que j’enseigne maintenant

Illustration d'un développeur Android et Kotlin Multiplatform pointant un schéma d'architecture clair avec une interface de service et son implémentation repository

Une interface se justifie. Sinon, elle ne se crée pas.

Deux cas où elle se justifie, dans mon expérience :

  1. Une frontière d’architecture. Par exemple dans une architecture multi-module, la couche domain ne doit pas avoir de dépendance forte à la couche data. Un LicenceDataSource qui parle à une API REST ou à une base SQLite est un détail d’implémentation que le domain ne doit pas connaître. Peu importe qu’il n’existe qu’une seule implémentation aujourd’hui : la frontière justifie l’interface à elle seule.
  2. Un design pattern qui l’exige. Le Strategy pattern, par exemple. Tu as plusieurs algorithmes interchangeables (stratégie de retry, mode de tri) et le code appelant doit ignorer lequel tourne. Là, l’interface porte le contrat du pattern.

En dehors de ces deux cas : classe concrète, appel direct, code lisible.

La checklist avant de créer une interface

Deux questions, dans l’ordre :

  1. Suis-je à une frontière d’architecture qui doit rester stricte ? Domain qui isole data, dépendance à une librairie externe… Une seule implémentation aujourd’hui ne change rien : la frontière justifie l’interface.
  2. Ai-je plusieurs implémentations réelles, imposées par un design pattern comme Strategy ? Pas « peut-être un jour ». Réelles, ou sur la roadmap produit.

Une seule réponse « oui » suffit à justifier l’interface. Zéro réponse « oui » : classe concrète.

Le refacto n’est pas un échec

Illustration d'un développeur Android et Kotlin Multiplatform consultant une vue diff de code montrant l'introduction d'une interface lors d'un refacto justifié

Six mois plus tard, une nouvelle demande produit arrive. GetLicenceUseCase, sans frontière d’architecture ni pattern au départ, doit maintenant gérer deux types de licence avec des règles différentes. Tu introduis l’interface à ce moment-là.

Ce n’est pas un raté de conception initiale. C’est le refacto qui fait son travail : répondre à un besoin réel, au moment où il existe, plutôt qu’à un besoin imaginé six mois avant qu’il n’apparaisse.

YAGNI (You Are not Gonna Need It) et refacto marchent ensemble. Le premier t’évite de coder pour un futur hypothétique. Le second te permet d’ajuster quand ce futur devient présent.

Le tableau qui résume tout

SituationInterface justifiée ?
Composant à la frontière de la Clean Architecture qui permet de respecter la règle des dépendances, même avec une seule implémentationOui
Implémentation d’un Design Pattern qui repose sur une abstractionOui
« On pourrait avoir besoin de changer un jour » sans frontière d’architecture ni pattern derrièreNon
Composant interne d’une couche de la Clean Architecture sans frontière ni patternNon
Faut-il bannir les interfaces en Kotlin ?

Non. Il faut les réserver aux cas où elles servent une raison précise : une frontière d’architecture ou un design pattern. Le reste peut rester en classe concrète.

Le principe d’inversion de dépendance impose-t-il une interface par classe ?

Non. Il impose de dépendre d’abstractions pour les éléments variables ou externes, pas de transformer chaque classe en contrat séparé.

Comment repérer une interface injustifiée dans une codebase existante ?

Cherche les interfaces avec une seule implémentation, sans frontière d’architecture claire ni pattern derrière. Ce sont des candidates à la suppression.

Sanders
➡️ Rejoins la newsletter. Reçois du contenu que tu ne trouveras nulle part ailleurs.

Si tu as aimé l'article, tu es libre de le partager! 🙂

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire