Refactoring : technique d’amélioration d’un code source

Au fil du développement d’une application, le code source accumule des lignes de code à la structure « sale », qui menacent les possibilités d’application et la compatibilité d’un logiciel. Pour résoudre ce problème, on peut soit écrire un code source entièrement nouveau, soit restructurer le code par petites étapes. Les programmeurs et les entreprises choisissent de plus en plus le refactoring, ou réusinage de code, pour optimiser à long terme un logiciel fonctionnel et favoriser sa lisibilité et sa clarté pour les autres programmeurs.

Quand on réusine un code, il faut se demander quels problèmes résoudre et avec quelle méthode. Désormais, le refactoring fait partie des bases pour apprendre la programmation et prend de plus en plus d’importance. Quelles méthodes faut-il mettre en œuvre et avec quels avantages et inconvénients faut-il composer ?

Qu’est-ce que le refactoring ?

La programmation d’un logiciel est un processus fastidieux, auquel peuvent participer plusieurs développeurs. Ainsi, le code source rédigé est souvent retravaillé, modifié et complété. Par manque de temps ou à cause de pratiques désuètes, le code source accumule des lignes sales, que l’on appelle « code smells ». Ces points faibles développés au fil du temps menacent les possibilités d’application et la compatibilité d’un programme. Pour éviter l’érosion et la dégradation progressives d’un logiciel, il faut passer par le réusinage du code.

On peut comparer le refactoring à la relecture d’un livre. Sans produire un livre entièrement nouveau, la relecture aboutit à un texte qui sera plus compréhensible. Tout comme pour la relecture, qui emploie différentes approches comme raccourcir une phrase, reformuler, supprimer ou restructurer, le refactoring s’appuie sur plusieurs méthodes comme l’encapsulation, le reformatage ou l’extraction pour optimiser un code sans altérer son fonctionnement.

Ce processus est nettement plus efficace en termes de coûts que la rédaction d’un code entièrement nouveau. Pour le développement de logiciels itératif et incrémentaux, comme le développement logiciel agile, le refactoring est capital, car dans ce modèle cyclique, les programmeurs apportent continuellement des modifications à leur logiciel. Le refactoring est une étape de travail constante.

Quand un code source s’érode : le code spaghetti

Il faut tout d’abord comprendre comment un code peut se modifier et muter en un tristement célèbre code spaghetti. Délais trop courts, manque d’expérience ou directives pas assez claires : en intégrant des instructions inutilement compliquées, la programmation d’un code entraîne des pertes de fonctionnalité. Plus son domaine d’application est rapide et complexe, plus la dégradation du code prend de l’ampleur.

Le terme code spaghetti désigne un code source brouillon et illisible, dont le langage de programmation est difficile à comprendre. Exemples simples de code brouillon : les sauts inconditionnels (goto) superflus, qui donnent l’instruction au programme de sauter d’un endroit à l’autre du code source. Citons aussi les boucles for/while et les instructions if superflues.

Ce sont particulièrement les projets auxquels participent de nombreux développeurs logiciels qui ont tendance à intégrer du texte source incompréhensible. Lorsqu’il passe entre plusieurs mains, et lorsqu’il contient déjà des faiblesses de départ, le code source intègre de plus en plus de liens prenant la forme de « solutions de contournement » : une révision du code dispendieuse est presque inévitable. Dans les cas les plus extrêmes, le code spaghetti peut mettre en danger le développement complet d’un logiciel. Alors, même le refactoring ne peut plus résoudre le problème.

Moins graves, on distingue aussi les codes smells et la pourriture du logiciel. Avec l’âge, un code peut commencer à « sentir », au sens figuré, à cause de lignes sales. Les lignes difficiles à comprendre empirent avec l’intervention d’autres programmeurs ou l’ajout de compléments. Si, dès l’apparition des premiers codes smells, on n’entreprend aucun réusinage du code, celui-ci se dégrade visiblement et perd sa fonctionnalité : il pourrit (de l’anglais « code rot »).

Quel est l’objectif du refactoring (ou réusinage de code) ?

Le but du réusinage du code est purement et simplement de produire un meilleur code. Un code efficace permet de mieux intégrer de nouveaux éléments, sans générer de nouvelles erreurs. Si la lecture du code ne leur demande pas d’effort particulier, les programmeurs s’y retrouvent plus vite et peuvent corriger ou éviter les bugs plus facilement. Le refactoring a également pour objectif de simplifier l’analyse des erreurs et la maintenabilité d’un logiciel. Les programmeurs voient leur tâche allégée lorsqu’ils contrôlent un code source.

Quelles sources d’erreurs le refactoring permet-il de corriger ?

Les techniques employées pour réusiner un code sont aussi variées que les erreurs qu’elles doivent corriger. Au fond, le refactoring se définit en fonction des erreurs et fait apparaitre les étapes nécessaires pour raccourcir ou supprimer une méthode de résolution. Les sources d’erreurs que l’on peut corriger lors d’un réusinage du code sont, entre autres :

  • Les méthodes brouillonnes ou trop longues : les lignes et blocs de commande sont tellement longs que les personnes extérieures ne peuvent pas comprendre la logique interne du logiciel.
  • Les doublons (redondances) : un code superflu contient souvent des redondances qui, à l’étape de la maintenance, doivent être modifiées séparément à chaque entrée. Elles génèrent donc du temps de travail et des coûts supplémentaires.
  • Les listes de paramètres trop longues : au lieu de répondre directement à une méthode, les objets voient leurs attributs transmis à une liste de paramètres.
  • Les classes avec un trop grand nombre de fonctions : les classes comprenant de trop nombreuses fonctions définies comme méthodes, également connues sous le nom d’objet dieu, qui rendent presque impossible toute modification du logiciel.
  • Les classes avec trop peu de fonctions : les classes comprenant trop peu de fonctions définies comme méthodes, qui sont superflues.
  • Les codes trop généraux avec des cas spécifiques : les fonctions avec des cas particuliers trop spécifiques, qui ne se produisent que rarement ou jamais et compliquent l’ajout de compléments nécessaires.
  • Les Middle Men : une classe distincte fait office d’intermédiaire entre les méthodes et différentes classes, alors que les méthodes pourraient appeler directement une classe.

Quand on réusine un code, quelle démarche adopter ?

Le refactoring doit toujours avoir lieu avant de modifier une fonction du programme. Le mieux est de procéder par toutes petites étapes et de tester les modifications apportées au code avec des processus de développement de logiciels, comme le développement piloté par les tests (TDD pour Test Driven Development) ou l’intégration continue (CI pour Continuous Integration). En résumé, le TDD et la CI recommandent de tester continuellement les petites sections de code nouvellement ajoutées, que les programmeurs créent, intègrent et contrôlent sur le plan fonctionnel par des tests automatisés et fréquents.

La règle d’or : modifier le programme de l’intérieur par petites étapes sans toucher aux fonctions extérieures. Après chaque modification, lancer un cycle de test aussi automatisé que possible.

Quelles techniques existent ?

Il existe de très nombreuses techniques concrètes de refactoring. Les travaux foisonnants de Martin Fowler et Kent Beck en proposent un aperçu complet : « Refactoring: Improving the Design of Existing Code ». En voici un court résumé :

Développement rouge-vert

Le développement rouge-vert est une méthode pilotée par les tests du développement logiciel agile. Elle s’applique lorsque l’on souhaite intégrer une nouvelle fonction à un code existant. Le rouge symbolise le premier cycle de tests, avant l’implémentation de la nouvelle fonction dans le code. Le vert symbolise la section de code la plus simple possible et nécessaire à cette fonction pour réussir le test. Il en résulte une extension avec des cycles de tests en continu pour résoudre les lignes de code erronées et augmenter la fonctionnalité. Le développement rouge-vert est un pilier du refactoring en continu dans le cadre d’un développement logiciel également en continu.

Branching-by-Abstraction

Cette méthode de réusinage du code décrit une modification d’un système par étapes et l’adaptation d’anciennes lignes de code implémentées aux nouvelles sections de code. La technique Branching-by-Abstraction est généralement utilisée lors de grandes modifications portant sur la hiérarchie des classes, l’hérédité ou l’extraction. L’implémentation d’une abstraction qui reste liée à une ancienne implémentation permet de relier d’autres méthodes et classes à l’abstraction et de remplacer la fonctionnalité de l’ancienne section de code par cette même abstraction.

On arrive souvent à ce résultat en utilisant les méthodes pull-up ou push-down. Elles créent un lien entre une nouvelle fonction, de meilleure qualité, et l’abstraction, tout en orientant les liens vers celle-ci. Ainsi, on peut rediriger une classe inférieure vers une classe supérieure (pull-up), ou bien les composants d’une classe supérieure vers une classe inférieure (push-down).

On peut enfin supprimer les anciennes fonctions sans prendre de risque pour la fonctionnalité de l’ensemble. Grâce à ces petites modifications, le fonctionnement du système reste inchangé, tandis que l’on peut remplacer les lignes de code sales par des lignes de code propres, section par section.

Compiler des méthodes

Le refactoring doit rendre aussi lisibles que possible les méthodes du code. Dans le meilleur des cas, dès la lecture, même un programmeur extérieur doit être capable de comprendre la logique interne d’une méthode. Pour compiler efficacement les méthodes, le réusinage du code propose différentes techniques. L’objectif de chaque modification est de distinguer chaque méthode, de supprimer les doublons et de diviser les méthodes les plus longues en éléments distincts pour permettre leur modification ultérieure.

Voici des exemples de techniques adéquates :

  • Extraire les méthodes
  • Ajouter inline à la méthode
  • Supprimer les variables temporaires
  • Remplacer les variables temporaires par la méthode des requêtes
  • Insérer des variables de description
  • Dissocier les variables temporaires
  • Supprimer les attributs des variables de paramètre
  • Remplacer une méthode par une méthode-objet
  • Remplacer l’algorithme

Décaler les propriétés entre différentes classes

Pour améliorer un code, on doit parfois décaler les attributs ou méthodes d’une classe à l’autre. Voici les techniques adaptées :

  • Décaler une méthode
  • Décaler un attribut
  • Extraire une classe
  • Ajouter inline à la classe
  • Cacher les délégués
  • Supprimer une classe intermédiaire
  • Insérer une méthode externe
  • Insérer une extension locale

Organisation des données

Cette méthode a pour objectif de répartir les données au sein des classes et de les maintenir aussi courtes et claires que possible. Les liens inutiles entre les classes, qui entravent le bon fonctionnement du logiciel dès la moindre modification, doivent être supprimés et répartis entre les classes adéquates.

Exemples de techniques adaptées :

  • Encapsuler les accès propres à l’attribut
  • Remplacer un attribut propre par une référence à un objet
  • Remplacer une valeur par une référence
  • Remplacer une référence par une valeur
  • Associer les données observables
  • Encapsuler les attributs
  • Remplacer un ensemble de données par une classe de données

Simplifier les expressions conditionnelles

Au cours du refactoring, il faut, autant que possible, simplifier les expressions conditionnelles. Exemple des techniques adaptées :

  • Scinder les relations
  • Fusionner les expressions conditionnelles
  • Fusionner les instructions répétées dans les expressions conditionnelles
  • Supprimer les boutons de contrôle
  • Remplacer les conditions imbriquées par des gardiens
  • Remplacer les distinctions par des polymorphismes
  • Insérer des objets nuls

Simplifier l’invocation des méthodes

L’invocation des méthodes gagne en rapidité et en simplicité grâce aux techniques suivantes, entre autres :

  • Renommer les méthodes
  • Ajouter des paramètres
  • Supprimer des paramètres
  • Remplacer des paramètres par des méthodes explicites
  • Remplacer le code erroné par des exceptions

Exemple de refactoring : renommer les méthodes

Dans l’exemple suivant, on voit bien que, dans le code source, le nom de la méthode ne renseigne pas clairement et rapidement sur sa fonctionnalité. La méthode doit retourner le code postal contenu dans l’adresse d’un bureau, mais le code ne le reflète pas clairement. Pour gagner en précision dans la formulation, le réusinage du code préconise de renommer la méthode.

Avant :

String getPostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getPostalCode());

Après :

String getOfficePostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getOfficePostalCode());

Refactoring : quels avantages, quels inconvénients ?

Avantages Inconvénients
Une meilleure compréhension facilite la maintenance et les possibilités d’extension du logiciel. Un manque de précision lors du refactoring peut implémenter de nouveaux bugs et de nouvelles erreurs dans le code.
La restructuration du code source peut se faire sans modifier le fonctionnement. Il n’existe pas de définition précise de ce qu’est un « code propre ».
Une meilleure lisibilité accroît la compréhension du code pour les autres programmeurs. Bien souvent, le client ne voit pas la différence lorsque le code est amélioré, car le fonctionnement reste identique : le gain n’est donc pas évident.
Retirer les redondances et les doublons améliore l’efficacité du code. Dans de grandes équipes travaillant sur le réusinage du code, la coordination peut exiger un temps étonnamment long.
Des méthodes indépendantes évitent que les modifications locales aient un impact sur une autre partie du code.  
Un code propre, avec des méthodes et des classes courtes et indépendantes, se caractérise par une plus grande facilité de test.  

Le principe de base du refactoring est le suivant : ajouter de nouvelles fonctions uniquement si celles-ci ne modifient pas le comportement initial du code source. On peut apporter des modifications au code source (refactoring) uniquement si celles-ci ne créent pas de nouvelle fonction.