Solid – D : Dependency inversion

Voici venu le temps des rires, des chants et du dernier article concernant les principes « SOLID », j’ai nommé: la « dependency inversion» ou, pour les anglophobes, l’inversion de dépendance.

À ne pas confondre avec l’injection de dépendance, même si on n’en est « pas loin », nous verrons pourquoi. Pour comprendre le principe voyons d’abord ce qu’est réellement une dépendance.

Team spirit!

Prenons comme exemple deux équipes de programmeurs qui travaillent sur le même projet.

  • L’équipe A s’occupe de créer une classe SmtpMailer,  qui va envoyer un e-mail via SMTP. Une classe d’assez bas niveau donc.
  • L’équipe B s’occupe de créer une classe PromoSender qui va récupérer la dernière promotion et va l’envoyer par e-mail via SmtpMailer.

Pour faciliter la lecture, je vais simplifier le code au maximum.

La classe de la deuxième équipe utilise la classe « Mailer » et donc en dépend. En effet, si un jour on remplace la classe « SmtpMailer », il y a fort à parier que le code de la seconde équipe devra aussi changer.

Voici le code à l’utilisation:

Le lecteur averti que vous êtes, remarquera que nous avons utilisé le principe d’injection de dépendance pour « donner » SmtpMailer à PromoSender. Nous parlons bien d’injection et pas encore d’inversion.

En Uml, cela donnerait ceci:

UserLog uses User

Remarquez la direction de la flèche qui va vers SmtpMailer. La classe PromoSender ne peut pas vivre sans cette dernière car elle l’utilise (ou en dépend si vous préférez). Donc si vous voulez envoyer un e-mail avec une autre classe que SmtpMailer, vous êtes cuit! A moins que…

Première étape: l’abstraction

Tout d’abord, voyons ces 2 classes comme faisant partie de 2 packages différents. Je rajoute les couleurs au diagramme pour vous simplifier la vie.

PromoSender uses SmtpMailer

En vert: Equipe A
En orange: Equipe B

Pour faire une « inversion » (et accessoirement bien faire les choses), nous utilisons une interface. L’équipe B se charge simplement d’extraire les méthodes publiques dans une interface qu’ils nomment MailerInterface. Le Sender va alors utiliser l’interface MailerInterface plutôt que la classe concrète: C’est une bonne chose!

En vert: Equipe A
En orange: Equipe B

Notez une chose intéressante à propos de la dépendance par rapport à SmtpMailer: Nous l’avons inversé (Le mot est lâché) ! À présent une flèche part de (et non plus vers) SmtpMailer. Les deux classes se rejoignent sur MailerInterface.

Dans le code, vous vous doutez bien que c’est tout simple. L’équipe A crée l’interface, et l’implémente:

Et pour l’équipe B, il suffit d’indiquer l’interface plutôt que la classe concrète dans le typage de la méthode:

L’essentiel est de ne pas dépendre de l’implémentation, mais d’une abstraction. Grâce à cela, l’implémentation (la classe SmtpMailer) peut être remplacée si votre projet évolue. Libre à vous de créer des ImapMailerGmailMailer, SimulatorMailer etc… tant que ceux-ci implémentent l’interface MailerInterface. Au final, peu importe votre type d’envoi, le code ne bougera pas:

C’est réutilisable et en plus ca répond au principe de l’O – Open/closed!

Voilà LA grande étape de l’inversion de dépendance. Pour résumer grossièrement: il faut utiliser des interfaces. Cependant, il y a deux choses qui ne semblent pas nettes.

  • Tout d’abord, lors du chapitre I – Interface Segregation, nous avons vu qu’une méthode/classe ne devait recevoir que ce dont elle avait besoin, ni plus ni moins. Or, PromoSender n’utilise pas le quart de ce que propose MailerInterface.
  • Ensuite, pour bien faire, les 2 modules devraient pouvoir être utilisables séparément. Si c’est bien le cas pour le package du « Mailer », c’est nettement moins vrai pour PromoSender qui reste dépendant de l’interface d’un autre package.

Voici peut-être venu le moment idéal pour lire la définition exacte du principe d’inversion de dépendance. Que dit Bob Martin (aka Uncle Bob) à ce sujet?

Bob a dit…

High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.

En français, cela donne:

Les modules de haut niveau ne devraient pas dépendre des modules de bas niveau. Les 2 devraient dépendre d’abstraction.
Les abstractions ne devraient pas dépendre de détails (=de l’implémentation). Les détails (=implémentations) devraient dépendre d’abstractions.

En gros, Bob nous dit qu’il va falloir revoir notre copie. Et ce, pour deux raisons:

  • Le module de haut niveau (PromoSender) dépend directement d’un module de bas niveau (MailerInterface).
  • PromoSender n’a pas sa propre abstraction. Elle utilise une interface d’un autre module et est donc dépendante de l’ « implémentation » de MailerInterface. En effet, si les méthodes de cette dernière changent, PromoSender doit changer également. On a vu mieux au niveau du « couplage ».

Chacun chez soi et les hippopotames seront bien gardés

Comment fait-on pour régler ces problèmes? Commençons par le plus simple: Demandons à l’équipe B (aka l’équipe des losers sans interface), de bien vouloir créer une interface que va utiliser PromoSender dans son constructeur. Cela afin de ne plus être dépendant de MailerInterface. On en profitera pour faire des méthodes qui nous « arrangent ».

Et en UML, cela donne:

Les 2 modules sont maintenant indépendants, les interfaces sont bien définies (et différentes), les tests unitaires sont probablement encore plus simples (on ne mock que le nécessaire, comme on a dit lors du I-Interface Segregation)… Mais une chose saute aux yeux: ils sont tellement indépendants qu’ils ne savent plus communiquer ensemble! Nous avons, comme qui dirait, un problème 🙂

Il existe une solution pour faire communiquer 2 modules qui « ne parlent pas la même langue ». Cette solution, nous l’avons déjà vue lors d’un précédent article, c’est bien sûr, le pattern Adapter. Encore lui! Décidément, il est partout. On le voit  d’ailleurs de plus en plus. Surtout dans des projets comme Symfony, Laravel où tout (ou presque) est modulable, externalisable voire même réutilisable en standalone (twig, swiftmailer, oAuth, etc…).

Le but du jeu est de pouvoir utiliser un objet « MyAdapter » comme s’il était un PromoSenderTransportInterface, et qu’il envoie un mail via le MailerInterface choisi.

Ça peut paraitre compliqué, mais ça ne l’est pas. Voici son code où il fait vraiment office de passerelle entre les 2 modules en implémentant une interface et en utilisant l’autre.

A l’utilisation, voici ce que ça donne avec une comparaison avant/après:

Voilà à quoi cela va ressembler en UML. Vous remarquerez que les flèches partent de MyAdapter, les 2 modules ne se connaissent toujours pas:

Comme vous pouvez le voir, l’adaptateur ne fait partie d’aucun des 2 modules. Cependant, il faut bien le mettre quelque part, mais où?

  • Au mieux, il sera créé dans un package à part: celui de votre projet.
  • Au pire, s’il faut vraiment choisir un des deux modules, il pourra être implémenté dans le package où se trouve PromoSenderInterface. En effet, celui-ci est de plus haut niveau, ce qui lui permet de plus facilement « anticiper » ce qu’il va utiliser.
  • Par contre, MailerInterface est de très bas niveau, et ne peut pas anticiper tous les autres modules qui vont l’utiliser.

L’heure du bilan

Aujourd’hui, nous avons vu qu’il était important de bien séparer l’abstraction et l’implémentation.

Tout d’abord, parce que l’abstraction a beaucoup moins susceptible d’être modifiée que l’implémentation. Le code change beaucoup, que ce soit en « change » ou en « bug », mais les interfaces changent très peu.

Ensuite, parce cela vous donne une certaine flexibilité quant à l’objet que vous pourrez donner à vos méthodes. Si vous n’utilisez pas d’interface, vous basculerez vite dans l’héritage « insensé » du style ImapMailer qui étend SmtpMailer qui lui même étend GmailMailer. Sur ce point de vue là, c’est Liskov qui ne sera pas contente.

Séparer abstraction et implémentation, oui, mais pas seulement. En effet, nous avons vu que nous pouvions aussi séparer le code en 2 packages complètement indépendants (ce qui nous rappelle le premier fondement SOLID: S: Single Responsibility). Deux packages différents, rime avec ré-utilisabilité, testabilité, maintenabilité, et plein d’autres choses qui terminent pas « ité », mais aussi plus de cohésion, moins de couplage et donc probablement moins de bugs. On sait également qu’au moins il y a de code, au moins vous rencontrerez de problèmes.

Nous avons vu, encore une fois, la magie de l’adaptateur. Celui-ci n’a pas que des avantages, car il peut aussi complexifier les choses, mais il a le mérite de coller des morceaux entre eux. Certes, si une de nos interfaces venait à être modifiée, il faudra changer l’adaptateur ainsi que ses 2-3 tests unitaires, soit! Mais c’est tout de même mieux que de revoir tout le code de votre package, et tous les malheureux projets qui l’utilisent. Sans parler des tests unitaires qui ne passent plus car plus aucun de vos « mocks » ne sont corrects. Ça fait beaucoup de boulot, vous ne trouvez pas?

Même si cet article n’est pas là pour faire l’apologie de l’adaptateur, sachez qu’il est tout autour de vous, mais qu’il ne s’appelle pas forcément Adapter partout. Voici un simple exemple de comment font les gens pour utiliser Smarty à la place de Twig dans Symfony: via EngineInterface. Vous verrez que la manière de procéder est exactement la même: On crée une classe qui étend une interface (EngineInterface) qui va utiliser un objet d’un autre module (Smarty ), et le tour est joué! Bien évidemment, Smarty et Symfony continuent d’évoluer indépendamment.

SOLID: Fin

Voilà pour le dernier article de SOLID. J’espère que tout cela vous a plu.

Comme vous avez pu le constater, les principes se chevauchent un peu les uns les autres. Au final, en suivant ces quelques règles, il est difficile de dévier de la trajectoire de « bon développeur ».

Cependant, comme dit Ribery: « SOLID, c’est une boite à outils qu’elle est bien de connaitre et de se rappeler ». Il ne s’agit donc pas là d’une vérité absolue. Je ne pense pas qu’un projet puisse être SOLID à 100%. Il ne faut pas tomber dans l’extrême où il existe une interface pour chaque signature de méthode. De même que chaque classe ne doit pas forcément avoir son interface. A l’époque, j’étais tombé sur ce ticket à propos de PimpleFabien Potencier disait:

Using an interface does not really make sense to me as I don’t see what kind of other implementation you might have.

Ça se tient! Quand savoir s’il faut une interface ou pas? Sans doute une question de feeling et d’expérience. L’exemple de l’ O: Open/Closed l’a bien montré: Le code, ça peut aussi se refactorer. Qui ne l’a jamais fait? Cela fait aussi partie de notre quotidien, et c’est comme ça que l’on apprend. C’est beau…

Rejoindre la conversation

12 commentaires

  1. Tes articles sur le SOLID sont vraiment excellent, j’ai rarement lu un truc aussi clair en français.
    J’ai hâte de voir la suite surtout si ça concerne les patterns qui sont encore trop abstraits pour moi.

  2. Super article. La pédagogie est un art qui vous est visiblement donné. J’ai parcouru plusieurs livres qui ne sont pas autant clairs. Merci

Laisser un commentaire

Répondre à Nicolas Annuler la réponse

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *