Nous avons eu récemment en interne un retour aux sources autour de la POO, et des principes SOLID. Celui-ci a donné lieu à compte rendu, dont la dernière phrase fut le point de départ d’une longue file de message que je vais tenter de résumer ici. Et pour les amoureux de la lecture, vous trouverez la copie intégrale à la fin de cet article.
La phrase en question : Il faut éviter de réaliser du SurDesign : Pourquoi mettre une interface lorsqu'il n'y a qu'une seule implémentation, pourquoi factoriser du code s'il n'est utilisé qu'à un seul endroit ?
Exposé d’un credo : si on fait une interface par implémentation, c’est pour faciliter les TU et l’IoC. Sauf que cela fait bien longtemps que les frameworks de mock permettent de mocker les classes elles-mêmes, et que l’IoC peut se faire via les implémentations directes, toute classe étant un type référence et donc initialisé à null par défaut.
Bref, il semblait que le pattern Interface/Implémentation soit obsolète, pour la plus grande joie de nombre d’entre nous. De plus, utiliser directement les implémentations permet aux outils de refactoring et d’analyse de code de ne pas se perdre (par exemple, si C1 appelle C2 qui appelle C3, on a soit une chaine de dépendance C1->C2->C3 si on utilise directement les implémentations, mais C1->I2 et C2->I3 avec des interfaces, on perd donc le lien entre C1 et C3).
Toutefois, utiliser les implémentations, cela force à tirer des dépendances entre les dll/war contenant lesdites implémentations. Ce qui implqiue que, par transitivité, à charger l’intégralité de ses assemblies à la moindre exécution de test unitaire(TU). En particulier, en cas d’utilisation de framework externes dont on ne maîtrise pas le code (ou qui peuvent faire des initialisations statiques), on peut se retrouver avec un TU qui fait des connexions à des bases de données, voire même affiche des pop-ups de connexion ! Dans ce cas, l’utilisation d’un assembly d’interfaces sans aucune dépendance permet d’éviter tous ces désagréments.
Un autre argument en faveur des interfaces est la facilité de contrôle de cette règle simple : « une implémentation = une interface et on n’utilise jamais directement les implémentations » via des outils automatiques, qui permet, sur un gros projet, de faire respecter à moindres frais un niveau de couplage très faible entre composants. Ça et interdire les new, bien sûr.
Oui mais, interdire les new, en pratique, ce n’est pas possible, puisqu’il y aura toujours des objets du framework à instancier, voire des classes privées, bref, on trouvera toujours de bonnes raisons de faire un new, non ?
Non, fait remarquer la vieille garde, c’est l’objectif de COM, qui remplace tous les new par ces appels à CoCreateInstance. Et la vieille garde (dont j’avoue faire partie), comme tous les vieux, quand on la lance sur son sujet favori, elle a du mal à s’arrêter.
Début de la parenthèse historique.
Donc, pourquoi COM ? Pourquoi un modèle aussi tordu où toute implémentation a une interface, où on fait des new sur les interfaces (merci VB) ? Pour cela, nous allons quelque peu revenir en arrière, à un temps que les moins de 20 ans ne peuvent pas connaître, comme le dit la chanson.
Or donc, il était une fois des exe et ces DLLs qui ne sont pas chargés dans le même espace mémoire. Mais pourquoi ? Et bien, parce qu’ainsi, on peut swapper sur disque chaque DLL, voire l’EXE, indépendamment, et quand on est sur un 286 avec 2 Mo de RAM, c’est nécessaire.
Du coup, conséquence, quand on fait un new dans une DLL, si le delete a lieu dans une autre DLL ou dans l’exe, cela ne marche pas, parce que l’objet a été alloué dans l’espace mémoire de la DLL d’origine et qu’on tente de le désallouer dans l’espace mémoire de celui qui fait l’appel au delete, donc ça ne fonctionne pas.
Du coup, COM, avec ses compteurs de références, laisse l’objet responsable de sa propre destruction, et donc s’assure que ladite destruction se fera dans le même espace mémoire que la création, puisque celle-ci a été faite à travers CoCreateInstance qui est assez intelligent pour aller faire la création au bon endroit. Exit les problèmes de destructeur qui plante !
Ajoutez à cela que COM, en héritier de CORBA, se veut indépendant du langage de programmation, et on obtient ce principe gravé dans le marbre de COM (et donc de Windows) qui est une implémentation = au moins 2 interfaces (celle sur laquelle faire le new et IUnknown, qui contient les mécanismes d’incrément et de décrément du compteur de référence).
Fin de la parenthèse historique.
Le rapport avec le sujet initial ? C# et Java ont été conçus pour être compatibles avec COM, donc ce pattern se trouve dans les gènes de ces deux langages, pour ainsi dire. De plus utiliser des interfaces assure, ou en tous cas facilite grandement, la compatibilité descendante, puisqu’une interface, ce n’est rien d’autre qu’un pointeur vers une table de fonctions virtuelles, et ça, nombreux sont les langages savent le traiter.
Au final : faut-il ou non avoir une interface par implémentation ? A votre avis ?
La communauté OCTO
Proposition de principe :
GDU nous propose de réfléchir au principe suivant :
L'idée étant de DIVISER pour mieux régner. Moins tu as de fichiers à ouvrir pour corriger un problème plus la compréhension est aisée et donc par extension meilleure est la qualité (lisibilité, maintenabilité).
"On peut résoudre 1 fois 1000 problèmes ou 1000 fois un problème mais si c'est 1000 fois 1000 problèmes c'est foutu ^^" GDU
La démarche :
Pour affirmer/infirmer cette proposition nous avons progressé dans notre démarche en commentant chacun des principes de la démarche S.O.L.I.D de la conception orientée objet.
S : Single responsibility principle
O : Open/closed principle
L : Liskov substitution principle
I : Interface segregation principle
D : Dependency inversion principle