Bonnes pratiques en développement logiciel

Cours

Auteur·rice
Affiliation

Frédéric Santos

CNRS, Univ. Bordeaux, MCC – UMR 5199 PACEA

Dans ce chapitre introductif, commençons par lister quelques unes des bonnes pratiques générales de programmation. Il s’agit en grande majorité de conseils langage-agnostiques, qui restent tout aussi pertinents pour des projets écrits en Julia, Python, ou d’autres langages.

Un conseil général : ces bonnes pratiques ne doivent pas être vues comme des contraintes à respecter en vue de la diffusion d’un projet logiciel, mais également comme des façons utiles de travailler de manière plus rapide, plus efficace et plus reproductible. Il y a en effet toujours des bénéfices personnels et “égoïstes” à adopter de meilleures pratiques (e.g., Markowetz 2015) !

1 Pourquoi créer un package ?

Le fait même de créer un package plutôt que d’utiliser ou de disséminer une collection de scripts R isolés peut être considéré comme une bonne pratique !

  • En réunissant et empaquetant l’ensemble du code associé à un projet de recherche, vous rendrez le tout plus cohérent et plus facile à maintenir dans le temps.
  • Un package R n’est pas nécessairement destiné à être publiquement disponible : vous pouvez également gagner de grands bénéfices à en créer seulement pour votre usage personnel. En effet, cela peut vous permettre de centraliser et de factoriser des bouts de code que vous utilisez souvent, rendant ainsi leur modification plus aisée et plus impactante pour vos projets futurs.
  • Toutefois, l’intérêt majeur d’un package réside bien dans le fait de faciliter l’utilisation de votre code par autrui. Tout d’abord, bien sûr, en le documentant et l’organisant proprement. Mais pas seulement : empaqueter votre code permet de spécifier et de mieux gérer les dépendances à d’autres packages R, i.e., de permettre à votre code de s’exécuter dans un certain environnement fixé, plus facilement reproductible.

2 Versionner le code

Le développement d’un package s’étale en général sur le temps long, peut impliquer plusieurs collaborateurs, et passera par de nombreuses versions de test avant d’arriver à un produit final stable. Tracer finement l’avancée du travail grâce à un système de gestion de versions est l’une des pratiques pouvant changer le plus drastiquement votre travail quotidien.

Ces sytèmes gardent la trace (auteur, date, contenu) de chaque modification effectuée sur un ensemble de fichiers au format texte, permettent de revenir aisément à des versions antérieures, de comparer deux versions d’un même fichier, etc. Git est de loin l’outil de contrôle de versions le plus utilisé aujourd’hui, mais on peut également citer Mercurial, SVN, ou même des outils spécifiques intégrés à certains éditeurs de texte (comme le mode VC de l’éditeur Emacs).

Les avantages de l’utilisation de tels systèmes sont nombreux :

  • En comparant deux versions d’une même fonction (par exemple une version fonctionnelle et une version aboutissant à une erreur), vous pourrez plus aisément identifier l’origine d’un bug.
  • En utilisant des forges logicielles (par exemple GitLab.com), vous pourrez plus facilement collaborer à plusieurs sur un même projet.
  • Grâce à la possibilité de revenir facilement à des versions antérieures, et donc d’annuler en un clin d’oeil un ensemble complexe de modifications effectuées sur le code du package, vous pouvez plus aisément expérimenter en gardant l’esprit libre, et modifier plus agressivement votre code source sans pour autant prendre le moindre risque.
  • L’historique de développement du package est documenté de façon pérenne et détaillée par l’ensemble des commits effectués sur le package.

3 Factoriser le code

Factoriser son code consister à éviter la répétition de longs extraits de code R identiques à plusieurs endroits du package. Cela rend en effet le code difficile à maintenir : si l’extrait de code en question doit être corrigé ou amélioré, il devra donc être modifié à plusieurs endroits différents (et il faudra veiller à n’en oublier aucun !).

Pour ne pas se retrouver dans cette situation, on préfèrera transformer cet extrait de code R en une fonction, et simplement appeler cette fonction à chaque fois que ce sera nécessaire. Modifier cette fonction en un seul endroit sera alors suffisant, et bien plus facile !

Imaginons qu’une même opération statistique “A” soit à réaliser au début de deux fonctions myfun1() etmyfun2(). On évitera autant que possible la situation (schématique) suivante :

myfun1 <- function(x) {
    ## code nécessaire à l'opération "A"
    ## reste du code spécifique à myfun1
}

myfun2 <- function(x) {
    ## même code nécessaire pour l'opération "A"
    ## reste du code spécifique à myfun2
}

pour préférer celle-ci (voyez-vous pourquoi ?) :

operationA(x) <- function(x) {
    ## code de l'opération A
}

myfun1 <- function(x) {
    operationA(x)
    ## reste du code spécifique à myfun1
}

myfun2 <- function(x) {
   operationA(x)
   ## reste du code spécifique à myfun2
}

Plus généralement, il vaut mieux essayer de de découper finement les opérations implémentées dans un package en une multitude de plus petites fonctions, plutôt que de créer d’immenses fonctions d’un seul tenant. En rendant votre code plus modulaire et plus “fragmenté”, il sera bien plus aisé à maintenir dans le futur.

4 Penser à l’écosystème R

Surtout si votre package est destiné à être publiquement disponible, il est nécessaire de penser à la place qu’il va occuper au sein d’un écosystème R déjà très foisonnant (plus de 20.000 packages à ce jour !).

4.1 Faciliter les collaborations futures

Vous pourrez être amené à avoir, dès le départ ou plus tard dans la vie du package, un ou des co-auteurs pour votre travail d’écriture du code R1. Quelques précautions s’imposent alors pour faciliter le travail pour tout le monde.

  1. Suivre un style cohérent. Adoptez un style précis pour votre code R, et essayez au maximum de vous y tenir. Différents styles existent : le style tidyverse, le style Google, etc2. L’essentiel est que l’ensemble des auteurs du package suivent le même style, et harmonisent leurs pratiques. Notez que le package R {styler} permet de reformater automatiquement du code R selon un style spécifique, et pourra donc faciliter l’harmonisation de l’ensemble du code. Le package {styler} est aisément mobilisable dans Rstudio, via le menu “Addins”.
  2. Commenter le code. Commenter de manière concise les portions de code plus techniques peut se révéler utile pour vous-même comme pour vos co-auteurs. De manière générale, commenter raisonnablement l’ensemble du code reste une pratique utile, sans toutefois abuser de commentaires inutiles sur des sections triviales. Nommer judicieusement une variable, par exemple, peut souvent permettre d’éviter un commentaire plus verbeux.
  3. Code de conduite. De manière facultative, sur les projets de grande taille hébergés sur des forges logicielles et susceptibles de recevoir des collaborations spontanées, inclure un “Code de conduite” peut être utile pour détailler la manière de rejoindre le projet et d’apporter sa pierre à l’édifice.

4.2 Interactions avec les autres packages

  • Afin de limiter au maximum les conflits de nommage avec des packages R courants, veillez à choisir des noms pertinents et pas trop génériques pour les fonctions qui seront exportées, i.e. directement accessibles à l’utilisateur final.
  • De même, veillez à choisir un nom pertinent pour votre package… et à vérifier qu’il n’est pas déjà pris par un package hébergé sur le CRAN !
  • Autant que faire se peut, limitez les dépendances à d’autres packages R. Il semble difficile (voire presque impossible) de créer un package R qui ne dépende d’aucun autre, mais faire directement dépendre votre package d’une centaine d’autres n’est pas forcément conseillé (ce sont autant de possibles bugs qui échappent par la suite à votre contrôle !). Lorsque des fonctions R de base permettent d’arriver à un résultat proche de fonctions contenues dans des packages plus exotiques, privilégiez la solution la plus parcimonieuse en dépendances externes.

5 Tester automatiquement le code

En développant votre package sur la durée, vous serez probablement amené à modifier, corriger, enrichir régulièrement le code des diverses fonctions du package. Il peut arriver qu’une addition de quelques lignes de code en apparence anodines introduise en réalité une erreur grave qui faussera les résultats renvoyés par une fonction jusqu’ici parfaitement correcte : on parle alors de régression.

Pour s’assurer que l’ensemble des fonctions d’un package renvoient bien les résultats attendus, une bonne pratique est de tester le code, c’est-à-dire de prévoir une série de petits cas simples d’utilisation des fonctions, en vérifiant que le résultat qu’elles produisent est bien celui attendu. Si cela peut être réalisé manuellement lorsqu’il n’y a qu’une ou deux fonctions dans votre package, cela deviendra très rapidement impossible au fur et à mesure que votre package grossira. Il est donc pertinent d’automatiser ces vérifications en implémentant des tests unitaires.

Un test unitaire est une vérification automatisée de sections de code, qui seront exécutées individuellement avec des paramètres spécifiés, et dont le résultat obtenu avec la dernière version du code sera comparé à un résultat théorique attendu. Les tests unitaires doivent donc nécessairement être reproductibles, et doivent rester rapides à exécuter. Ils s’exécuteront automatiquement à chaque construction du package, et vous serez averti en cas de régression : cela vous permettra de revenir rapidement corriger la fonction fautive et de rétablir le fonctionnement normal du package.

En R, le package {testthat} est aujourd’hui la solution la plus populaire pour réaliser des tests unitaires automatiques, et assurer ainsi la qualité de votre code.

6 Penser à l’expérience utilisateur

6.1 Documenter exhaustivement le package

La seule documentation essentielle du package réside dans les pages d’aide accessibles à l’utilisateur final par la fonction help(). Ces pages d’aide doivent être aussi complètes que possible : outre la description précise des arguments et de la valeur retournée par chaque fonction, des références théoriques sont les bienvenues, ainsi que toute précision qui pourrait être utile à un nouvel utilisateur découvrant votre package.

Toutefois, d’autres méthodes plus “modernes” existent pour documenter votre package, en complément (et non en remplacement !) de la précédente.

  1. Concevoir une vignette. Une vignette est un document explicatif décrivant les fonctionnalités de votre package, en mélangeant des éléments théoriques, des exemples concrets et reproductibles d’utilisation de fonctions du package, et les sorties/résultats des blocs de code R associés. Par exemple, la vignette du package {corrplot} montre de manière presque exhaustive les fonctionnalités du package, et fournit à l’utilisateur final des extraits de code qu’il aura simplement à copier-coller et à adapter très légèrement à ses propres données.
  2. Concevoir un site web. Plus intéressant et plus complet encore : un site web entier peut être généré pour accompagner le package, le promouvoir et le documenter. Le site web du package {vegan} est un bon exemple. De tels sites web peuvent aisément être conçus avec le package R {pkgdown}.

Dans les deux cas, la vignette et le site web consisteront probablement en l’écriture de fichiers Rmarkdown ou Quarto, qui seront compilés au moment de la construction du package : tout le code qu’ils contiennent est exécuté à ce moment, et les résultats (sorties R, figures, tables, …) à inclure dans la vignette et le site web sont produits automaiquement. De cette façon, la documentation du packahe reste “toujours à jour”, automatiquement, sans intervention particulière de votre part.

6.2 Fournir des exemples

Pour que chacune de vos fonctions soit facilement compréhensible et utilisable, la mesure la plus simple reste d’inclure des exemples directement exécutables et reproductibles à la fin de la page d’aide de la fonction. Pour cela, il faudra se reposer sur l’un des jeux de données inclus dans R (iris, etc.), ou inclure vous-même un petit jeu de données dans votre package, permettant la conception d’exemples reproductibles dans la documentation.

Une inspiration possible : les exemples reproductibles inclus dans la documentation de la fonctionadonis2() du package {vegan}`.

6.3 Prévoir des messages d’erreur explicites

R n’est pas connu pour renvoyer systématiquement des messages d’erreur très explicites (en comparaison, les messages d’erreurs renvoyés par des langages plus modernes comme Julia sont souvent plus interprétables et plus précis). Dans votre package, pensez à remédier à ce problème en anticipant de possibles erreurs que pourraient faire les utilisateurs du package, et en écrivant des messages d’erreur clairs pour chaque cas.

La fonction stop() permet par exemple d’arrêter l’exécution d’une fonction en renvoyant un message d’erreur spécifié. Les fonctions warning() et message(), sans bloquer l’exécution, renvoient quant à elles des avertissements et des notes destinées à informer les utilisateurs de votre code.

Dans certains cas, R renvoie nativement des messages d’erreur suffisamment explicites, si vous adoptez les bonnes pratiques. Par exemple, prévoyez de tester la conformité des arguments d’entrée de vos fonctions grâce à la fonction match.arg(). Observez la situation suivante :

## Définir une fonction personnalisée :
f <- function(x, transform = c("center", "scale", "both")) {
    transform <- match.arg(transform)
    ## ...
    ## code R spécifique en fonction de la valeur de "transform"...
    return(x)
}

## Que se passe-t-il si on entre un argument "transform" incorrect ?
f(x = rnorm(10), transform = "toto")
Error in match.arg(transform): 'arg' should be one of "center", "scale", "both"

L’emploi de match.arg() assure ici que l’utilisateur final comprendra d’où vient son erreur. Pour des erreurs plus complexes, prévoyez vous-mêmes de générer vos messages d’erreurs.

7 Se faciliter la tâche

7.1 Utiliser un environnement de développement intégré (EDI)

Une bonne partie des principes détaillés ci-dessus sont bien plus simples à mettre en oeuvre en utilisant un EDI, qui vous assistera efficacement dans ces opérations. Pour R, l’EDI le plus utilisé est Rstudio (qui sera utilisé dans cette formation), mais il est important de souligner qu’il existe des alternatives libres et tout aussi efficaces. Par exemple, l’éditeur de texte Emacs propose EDE, un genre d’IDE basique langage-agnostique, et surtout ESS, un EDI spécifique pour R.

Parmi les nombreux avantages de ces outils :

Table 1: Quelques équivalences de fonctionnalités entre Rstudio et Emacs ESS.
Fonctionnalité Rstudio Emacs ESS
Créer et configurer un projet Projets .Rproj Mode projet
Naviguer dans de gros projets Raccourci Ctrl + . Raccourci C-x p f
Interagir avec Git Interface graphique Magit
Interface avec {devtools} Oui Oui
Raccourcis pour construire / tester le package Oui Oui

7.2 Utiliser un gestionnaire de tâches

Même s’il s’agit d’une question qui peut sembler secondaire, développer un package nécessite du travail sur le temps long, et une organisation efficace du travail. Des gestionnaires de tâches généralistes ou spécialisés peuvent vous aider à planifier le travail à effectuer sur le package, à garder en mémoire les bugs à corriger, et à n’oublier aucune tâche importante. Pour plus de détails (généraux) sur la question de l’organisation du travail et de la planification des tâches, on pourra par exemple consulter Allen (2003).

Des logiciels spécialisés pourront se révéler utiles : citons par exemple Logseq, Org (Dominik 2010), Redmine, etc.

7.3 Développement orienté par la documentation

On peut intuitivement penser qu’il est logique de commencer par l’écriture du code du package, et de tout documenter une fois le code terminé. En réalité, il peut pourtant être à la fois plus sûr et plus efficace d’orienter l’ensemble du développement par l’écriture de la documentation : dans cette approche, c’est bel et bien le travail de documentation qui est premier, et le travail d’écriture du code qui suit.

  • Dans cette formation, nous verrons plus tard comment écrire une vignette pour notre package, une fois que le code aura été écrit. Une approche alternative consiste à écrire la vignette au fur et à mesure de l’écriture du code, et de voir en réalité le travail de codage comme un moyen d’enrichir le plus possible la vignette de présentation du package.
  • Pour aller plus loin, {fusen} est un package R permettant de transformer un fichier Rmarkdown en package R : l’ensemble du travail de conception du package se fait alors en mode programmation lettrée (Knuth 1984), avant de transformer le notebook en package à la fin du travail. Une démonstration de {fusen} sera effectuée au cours de la formation.

Dans les deux cas, il s’agit de ne plus voir l’écriture de la documentation comme un exercice fastidieux et contraignant à repousser le plus longtemps possible, mais comme une manière de structurer et rythmer le développement du package.

Retour au sommet

Références

Allen, David. 2003. Getting Things Done: The Art of Stress-Free Productivity. New York: Penguin.
Dominik, Carsten. 2010. Org Mode 7 Reference Manual: Organize Your Life with GNU Emacs, Release 7.3. Bristol: Network Theory.
Knuth, Donald Ervin. 1984. « Literate Programming ». The Computer Journal 27 (2): 97‑111. https://doi.org/10.1093/comjnl/27.2.97.
Markowetz, Florian. 2015. « Five Selfish Reasons to Work Reproducibly ». Genome Biology 16 (1): 274. https://doi.org/10.1186/s13059-015-0850-7.

Notes de bas de page

  1. Selon le vieil adage des programmeurs, il y aura forcément au moins un collaborateur dans ce projet : votre future-self, qui devra pouvoir comprendre le travail que vous aurez fait aujourd’hui !↩︎

  2. Sur la question du style et des bonnes pratiques en programmation R, voir également un diaporama de Martin Maechler.↩︎