Duplication ou coïncidence ?
Giving a computer two contradictory pieces of knowledge was Captain James T. Kirk’s preferred way of disabling a marauding artificial intelligence. Unfortunately, the same principle can be effective in bringing down your code.
— « The Pragmatic Programmer », Dave Thomas, Andy Hunt.
Ainsi commence le chapitre 9 du fabuleux ouvrage de Dave Thomas et Andy Hunt, dont je vous recommande chaleureusement la lecture. Aujourd’hui je vous propose de nous arrêter un instant sur un principe très connu des développeurs que nous sommes et qui revient très régulièrement dans nos discussions : DRY — Don’t Repeat Yourself.
Rien n’est gravé dans le marbre
Si nous créons des logiciels, c’est en premier lieu pour répondre à un besoin. Un besoin métier, exprimé par notre client. Et ce client ça peut aussi être nous-même, ou notre employeur, peu importe. Ce qui importe en revanche, c’est de répondre à ce besoin qui nous a été exprimé. Et soyons lucides, ce besoin peut évoluer ; il va évoluer, parfois fréquemment, parfois non, parfois substantiellement, parfois de presque rien, mais il évoluera. Et ce qui fera la qualité du logiciel que nous aurons produit, ce sera sa capacité d’adaptation à ces changements qui vont immanquablement se présenter tout au long sa vie.
Pour paraphraser Dave Thomas & Andy Hunt, pour être en capacité de s’adapter rapidement et efficacement, chaque besoin exprimé devra trouver corps dans une implémentation unique et non ambiguë, faisant autorité à travers toute l’application. En d’autres termes, il faudra nous préserver des duplications diverses, implémentations multiples et autres dissonances. Faire la chasse aux doublons. Ne pas se répéter.
Qu’entendons-nous par DRY ?
Don’t Repeat Yourself. Ne pas se répéter. Jamais. Tout le monde vous le dira : la répétition c’est mal. Et bien souvent on applique ce principe à la lettre, sans jamais l’interroger. De prime abord, il a l’air plein de bon sens. Mais parfois, j’ai le sentiment qu’il ne sonne pas juste. J’ai l’impression qu’il y a la bonne et la mauvaise répétition ; que cette répétition-là n’est pas si fautive, que celle-ci se justifie. Alors posons-nous la question : Quel objectif se cache derrière ce principe ? Et que ne faut-il pas répéter au juste ?
Duplication de code
Notre premier réflexe sera de rechercher et d’identifier des duplications de code afin de les éliminer sans autre forme de procès. Or, l’application du principe DRY implique une réunification de plusieurs portions de code, et cela créera soit de la complexité, soit du couplage. Souvent un peu des deux.
Complexité involontaire
Voici l’illustration d’une fausse bonne idée introduisant une complexité superflue rendant pénible la lecture et l’interprétation du code.
# @param attrs [Hash]
def initialize(attrs)
Hash(attrs).each_pair do |k, v|
instance_variable_set(:"@#{k}", v)
end
end
L’exemple est volontairement assez bas niveau pour éviter d’avoir à présenter un extrait trop complexe, mais c’est à tous les niveaux de nos systèmes d’information que l’on peut retrouver ce genre d’abstraction qui, sous couvert de DRY, n’apporte rien d’autre que de la confusion. Mieux vaut ici lui préférer un peu de duplication subjective.
# @param attrs [Hash]
def initialize(attrs)
attrs.symbolize_keys!
@label = attrs[:label]
@iso_code = attrs[:iso_code]
@service_key = attrs[:service_key]
@accounting_category_id = attrs[:accounting_category_id]
@official_rule = attrs[:official_rule]
@official_value = attrs[:official_value]
@translation_rule = attrs[:translation_rule]
@translation_value = attrs[:translation_value]
end
Non seulement on comprend d’un seul coup d’œil ce que fait notre méthode, mais
on se prémunit d’un comportement inattendu. En effet, notre première
implémentation n’avait aucun garde-fou et des variables d’instance pouvaient
être créées en pagaille ! Cerise sur le gâteau, on peut maintenant faire des
recherches dans le code : si précédemment on avait recherché
@translation_rule
, on aurait fait choux blanc et ça nous aurait
potentiellement fait perdre pas mal de temps.
Couplage
Par ailleurs, le couplage introduit par l’unification, qui peut résulter en la création d’un nouveau service par exemple, implique que les différentes portions de code maintenant dépendantes de ce service ne pourront plus évoluer indépendamment. Faire évoluer ce service aura une répercussion sur chacune d’elles. Ce n’est pas indolore, et pas toujours souhaitable.
Quand l’intention prime
C’est là qu’entre en jeu l’intention. Se poser la question de l’intention est primordial si l’on souhaite faire transpirer dans notre code le besoin métier exprimé et conserver un code maintenable.
Ainsi, si deux besoins distincts répondant à des aspects différents du métier de notre client mènent à deux implémentations très similaires, voire identiques, alors il ne s’agit pas d’une duplication mais d’une coïncidence ! Rappelez-vous, rien n’est gravé dans le marbre. Ces besoins bien que très similaires aujourd’hui, pourront être amenés à évoluer dans des directions très différentes à l’avenir. En unifiant ces deux-là, l’intention serait floue, l’implémentation certainement plus complexe, et du couplage serait introduit, réduisant à néant nos efforts pour améliorer la maintenabilité de notre code.
DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two totally different ways.
— « The Pragmatic Programmer », Dave Thomas, Andy Hunt.
Savoir dire stop
Quand on travaille sur une base de code sur laquelle on souhaite apporter quelque évolution, il arrive un moment où l’on a besoin de toucher à une méthode, une classe, un service partagé entre plusieurs portions de code. Le premier réflexe est généralement d’y ajouter des conditions, ou de nouveaux arguments, un flag, ou une nouvelle signature de méthode pour les adeptes de programmation fonctionnelle, afin de gérer cette nouvelle situation. C’est à cet instant qu’il faut éloigner les mains du clavier et prendre un peu de recul pour s’assurer qu’on n’est pas en train de créer un monstre. Car ça peut vite tourner au cauchemar ! Imaginez un peu à quoi ce petit bout de code mutualisé pourrait ressembler après deux ou trois mouvements dans ce sens. Le code serait proprement incompréhensible. L’écriture de tests risque d’être un vrai casse-tête pour ne pas oublier un cas à la marge ; et la lecture de ceux-ci risque de donner un mal de crâne à quiconque s’y essaiera. Ah ! Et oubliez l’intention, elle est plurielle et a été oubliée il y a bien longtemps !
Il est peut-être temps de dire stop et de réintroduire de la soi-disant duplication pour réduire l’abstraction, réaffirmer les différentes intentions, diminuer le couplage, simplifier les tests et améliorer la maintenabilité de l’ensemble. Je le répète, DRY consiste à ne pas dupliquer l’expression d’une intention.
Duplication is far cheaper than the wrong abstraction
— Sandi Metz, RailsConf 2014.
DRY à tous les étages
Les violations du principe DRY peuvent se retrouver partout et dans toutes les couches de nos applications.
Des données normalisées
Prenons l’exemple des bases de données relationnelles. Respecter les formes normales lorsqu’on modélise nos différentes tables et leurs relations, revient à appliquer le principe DRY. En évitant toute redondance de données, on évite par la même occasion les inconsistances et les risques d’écritures partielles. Mais ici aussi, il peut être pertinent d’interroger ce dogme, notamment lorsqu’on manipule de grandes quantités de données et que des problèmes de performance commencent à poindre. Dans ce cas, on introduira volontairement de la dénormalisation pour limiter le nombre de jointures et gagner de précieuses secondes. Il faut donc faire preuve de discernement.
Une SOLID conception
Le S de SOLID se rapporte au principe de responsabilité unique.
A class should have only one reason to change
— Robert C. Martin
En respectant ce principe, on respecte aussi DRY ! En visant une seule responsabilité par classe, on va naturellement éviter de répéter une même intention à différents endroits du code puisqu’on aura à disposition une classe qui lui sera dédiée et que l’on pourra réemployer à loisir. Habile.
Notez que ce faisant on crée de la complexité, mais une complexité assumée et profitable sur le long terme car la maintenabilité du code en sera améliorée.
Et la doc, on en parle ?
La duplication n’a pas lieu que dans le code. Il n’est pas rare de tomber sur une méthode comme celle-ci :
# Finds a country by its ISO code.
# Raises an error if ISO code isn't a two-characters string
def find_country(code)
raise InvalidISOCodeError if code.length != 2
Country.find_by(iso_code: code)
end
Ici le problème concerne l’intention qui est déclarée deux fois : une fois dans le code, et de nouveau en commentaire. Si la règle métier change, alors le commentaire sera erroné. Et soyons honnêtes, ce commentaire n’apporte pas grand-chose, pour ne pas dire strictement rien que le code ne nous dit déjà.
Il y a deux points auxquels faire attention dans l’exemple ci-dessus. Le premier concerne le nommage de nos méthodes et autres variables. Bien nommées, elles sont alors explicites, l’intention est claire, et sont donc autosuffisantes. Le second point d’attention concerne la documentation de notre méthode. Celle-ci pourrait nous apporter de la valeur, notamment dans le cadre d’un langage non typé ou faiblement typé, comme c’est le cas de Ruby. Considérez ceci :
# Finds a country by its ISO code.
# @param iso_code [String] an ISO 3166-1 alpha-2 code
# @return [Country]
def find_country_by_iso_code(iso_code)
raise InvalidISOCodeError if iso_code.length != 2
Country.find_by(iso_code: iso_code)
end
À présent, la documentation de la méthode nous éclaire sur la nature des paramètres attendus et sur ce que nous retourne la méthode. Et bonus, cette syntaxe, qui est celle de Yard, nous permettra de générer la documentation automatiquement !
Précision supplémentaire, les plus zélés d’entre nous ont tendance à absolument tout documenter. Ce n’est pas forcément une bonne idée, notamment pour ce qui est des méthodes privées. En effet, celles-ci sont un peu notre tambouille interne et sont les plus susceptibles d’évoluer rapidement. Il est donc important de se demander si la documentation de telle ou telle méthode est vraiment nécessaire ; cela apporte-t-il vraiment quelque chose ? Ou est-ce que ça ne va pas simplement ajouter de la duplication et amoindrir la maintenabilité du code ?
DRY par-delà le code
Il arrive très souvent que plusieurs personnes ou plusieurs équipes travaillent de concert sur une même application. Et sans un bon chef d’orchestre, il arrive souvent que l’on se marche sur les pieds. On éprouvera le besoin de toucher à une partie du code qui, objet de toutes les convoitises, se trouve être en faveur auprès d’une autre équipe. Le susnommé chef d’orchestre articulera la coordination de tout ce petit monde autour de deux axes : communication et stratégie.
Bien communiquer
L’introduction de couplage par la réunification de code découlant de l’application du principe DRY, demandera davantage de communication. Cela peut se limiter à une nouvelle classe ou méthode qu’il faudra simplement nommer judicieusement et documenter le plus exhaustivement possible. Mais à plus large échelle, le besoin de communication se fera plus important. L’extraction du code fautif dans une nouvelle brique logicielle demandera, en plus d’une API correctement documentée, de multiples échanges entre les différentes parties prenantes, développeurs ou équipes, afin de s’assurer que les besoins de chacun sont satisfaits. Et aussi souvent que des évolutions seront nécessaires, il sera nécessaire de communiquer efficacement dessus, en amont comme en aval des développements.
Bien communiquer est essentiel à la bonne conduite d’un projet. Une communication fréquente et efficace permettra d’éviter nombre d’écueils plutôt que de les rencontrer au moment de fusionner les travaux. Mais cela peut ne pas suffire, c’est là qu’un bon outil de versionnage nous permettra le moment venu de gérer les conflits qui se présentent et trancher les différends.
En fin stratège
Mais ne pourrions-nous pas mieux nous prémunir de telles situations ? Si les deux équipes travaillent sur deux aspects distincts de l’application, en toute logique le principe DRY devrait nous y aider. Oui. Et non. En fait c’est peut-être très précisément l’application de DRY qui peut nous amener à de telles situations ! Rappelez-vous, l’unification entraîne immanquablement du couplage.
Alors comment ne pas tomber dans le piège du couplage ? Le Domain Driven Design peut être de bon secours dans ce genre de situation. Il introduit la notion de Bounded Context dont le but est d’étanchéifier les différents sous-domaines de notre application, évitant ainsi tout partage de code entre les différents contextes ainsi isolés.
Alors à quel moment appliquer DRY ?
On l’a dit, appliquer ce principe revient généralement à introduire de l’abstraction dans notre code. L’intérêt et le besoin de cette nouvelle couche d’abstraction devraient toujours être interrogés. Il peut arriver que l’on ait des difficultés à choisir s’il faut ou non s’attaquer à cette déduplication. Cela signifie bien souvent qu’il nous manque des clés de compréhension — quelles étaient les intentions initiales ? Quels besoins métier sont implémentés ici ? Dans ce genre de situation, mieux vaut généralement laisser la duplication en l’état et créer un ticket de dette technique pour en discuter avec les sachants du métier et, quand c’est possible, l’autrice ou l’auteur du code en question. En effet, un excès de zèle pourrait tout simplement nuire à la maintenabilité de l’application, voire à la lisibilité du code.
Il peut aussi arriver que l’on introduise de la duplication intentionnellement. Dans ce cas, il ne faut pas la voir comme une dette, mais davantage comme un signal. Ce type de scénario se présente généralement dans les premières phases d’implémentation d’un besoin métier. On voit bien qu’il partage certaines caractéristiques intrinsèques avec d’autres fonctionnalités de l’application, mais on s’interroge encore : s’agit-il vraiment de duplication ou d’une coïncidence ? Vais-je apporter de la souplesse ou davantage de contraintes en factorisant ? On peut alors choisir de s’autoriser à retarder la décision, attendre d’avoir plus de billes, et faire un choix éclairé une ou deux itérations plus tard.
Faciliter la réutilisation
En substance, DRY est un principe puissant à appliquer avec sagesse et discernement. Ce qui primera ce seront les décisions qui faciliteront la réutilisabilité de notre code, par nous même, par nos collègues, par toute personne amenée à faire évoluer le projet ou tout simplement à interagir avec. Que ce soit à travers l’utilisation d’une API, la lecture de la documentation, l’ajout, la suppression ou la modification d’une fonctionnalité, il nous faut faire l’effort constant de réduire les frictions et de lever les ambiguïtés. Notre code se doit de révéler les intentions qu’il tente d’exprimer. Les besoins auxquels il répond doivent être clairement identifiables. Et un changement de spécification, une norme qui évolue, ne devraient pas avoir de résonance à travers toute l’application.
Pour aller plus loin
- The wrong abstraction, Sandi Metz
- The Fallacy of DRY, Entropy Wins
- The Pragmatic Programmer Dave Thomas, Andy Hunt