Au fil du dé­ve­lop­pe­ment d’une ap­pli­ca­tion, le code source accumule des lignes de code à la structure « sale », qui menacent les pos­si­bi­li­tés d’ap­pli­ca­tion et la com­pa­ti­bi­lité d’un logiciel. Pour résoudre ce problème, on peut soit écrire un code source en­tiè­re­ment nouveau, soit res­truc­tu­rer le code par petites étapes. Les pro­gram­meurs et les en­tre­prises choi­sis­sent de plus en plus le re­fac­to­ring, ou réusinage de code, pour optimiser à long terme un logiciel fonc­tion­nel et favoriser sa li­si­bi­lité et sa clarté pour les autres pro­gram­meurs.

Quand on réusine un code, il faut se demander quels problèmes résoudre et avec quelle méthode. Désormais, le re­fac­to­ring fait partie des bases pour apprendre la pro­gram­ma­tion et prend de plus en plus d’im­por­tance. Quelles méthodes faut-il mettre en œuvre et avec quels avantages et in­con­vé­nients faut-il composer ?

Qu’est-ce que le re­fac­to­ring ?

La pro­gram­ma­tion d’un logiciel est un processus fas­ti­dieux, auquel peuvent par­ti­ci­per plusieurs dé­ve­lop­peurs. Ainsi, le code source rédigé est souvent re­tra­vaillé, 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é­ve­lop­pés au fil du temps menacent les pos­si­bi­li­tés d’ap­pli­ca­tion et la com­pa­ti­bi­lité d’un programme. Pour éviter l’érosion et la dé­gra­da­tion pro­gres­sives d’un logiciel, il faut passer par le réusinage du code.

On peut comparer le re­fac­to­ring à la relecture d’un livre. Sans produire un livre en­tiè­re­ment nouveau, la relecture aboutit à un texte qui sera plus com­pré­hen­sible. Tout comme pour la relecture, qui emploie dif­fé­rentes approches comme rac­cour­cir une phrase, re­for­mu­ler, supprimer ou res­truc­tu­rer, le re­fac­to­ring s’appuie sur plusieurs méthodes comme l’en­cap­su­la­tion, le re­for­ma­tage ou l’ex­trac­tion pour optimiser un code sans altérer son fonc­tion­ne­ment.

Ce processus est nettement plus efficace en termes de coûts que la rédaction d’un code en­tiè­re­ment nouveau. Pour le dé­ve­lop­pe­ment de logiciels itératif et in­cré­men­taux, comme le dé­ve­lop­pe­ment logiciel agile, le re­fac­to­ring est capital, car dans ce modèle cyclique, les pro­gram­meurs apportent con­ti­nuel­le­ment des mo­di­fi­ca­tions à leur logiciel. Le re­fac­to­ring est une étape de travail constante.

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

Il faut tout d’abord com­prendre comment un code peut se modifier et muter en un tris­te­ment célèbre code spaghetti. Délais trop courts, manque d’ex­pé­rience ou di­rec­tives pas assez claires : en intégrant des ins­truc­tions inu­ti­le­ment com­pli­quées, la pro­gram­ma­tion d’un code entraîne des pertes de fonc­tion­na­lité. Plus son domaine d’ap­pli­ca­tion est rapide et complexe, plus la dé­gra­da­tion du code prend de l’ampleur.

Le terme code spaghetti désigne un code source brouillon et illisible, dont le langage de pro­gram­ma­tion est difficile à com­prendre. Exemples simples de code brouillon : les sauts in­con­di­tion­nels (goto) superflus, qui donnent l’ins­truc­tion au programme de sauter d’un endroit à l’autre du code source. Citons aussi les boucles for/while et les ins­truc­tions if su­per­flues.

Ce sont par­ti­cu­liè­re­ment les projets auxquels par­ti­ci­pent de nombreux dé­ve­lop­peurs logiciels qui ont tendance à intégrer du texte source in­com­pré­hen­sible. Lorsqu’il passe entre plusieurs mains, et lorsqu’il contient déjà des fai­blesses de départ, le code source intègre de plus en plus de liens prenant la forme de « solutions de con­tour­ne­ment » : une révision du code dis­pen­dieuse est presque iné­vi­table. Dans les cas les plus extrêmes, le code spaghetti peut mettre en danger le dé­ve­lop­pe­ment complet d’un logiciel. Alors, même le re­fac­to­ring ne peut plus résoudre le problème.

Moins graves, on distingue aussi les codes smells et la pour­ri­ture du logiciel. Avec l’âge, un code peut commencer à « sentir », au sens figuré, à cause de lignes sales. Les lignes dif­fi­ciles à com­prendre empirent avec l’in­ter­ven­tion d’autres pro­gram­meurs ou l’ajout de com­plé­ments. Si, dès l’ap­pa­ri­tion des premiers codes smells, on n’en­tre­prend aucun réusinage du code, celui-ci se dégrade vi­si­ble­ment et perd sa fonc­tion­na­lité : il pourrit (de l’anglais « code rot »).

Quel est l’objectif du re­fac­to­ring (ou réusinage de code) ?

Le but du réusinage du code est purement et sim­ple­ment 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 par­ti­cu­lier, les pro­gram­meurs s’y re­trou­vent plus vite et peuvent corriger ou éviter les bugs plus fa­ci­le­ment. Le re­fac­to­ring a également pour objectif de sim­pli­fier l’analyse des erreurs et la main­te­na­bi­lité d’un logiciel. Les pro­gram­meurs voient leur tâche allégée lorsqu’ils con­trô­lent un code source.

Quelles sources d’erreurs le re­fac­to­ring permet-il de corriger ?

Les tech­niques employées pour réusiner un code sont aussi variées que les erreurs qu’elles doivent corriger. Au fond, le re­fac­to­ring se définit en fonction des erreurs et fait ap­pa­raitre les étapes né­ces­saires pour rac­cour­cir ou supprimer une méthode de ré­so­lu­tion. Les sources d’erreurs que l’on peut corriger lors d’un réusinage du code sont, entre autres :

  • Les méthodes brouil­lonnes ou trop longues : les lignes et blocs de commande sont tellement longs que les personnes ex­té­rieures ne peuvent pas com­prendre la logique interne du logiciel.
  • Les doublons (re­don­dances) : un code superflu contient souvent des re­don­dances qui, à l’étape de la main­te­nance, doivent être modifiées sé­pa­ré­ment à chaque entrée. Elles génèrent donc du temps de travail et des coûts sup­plé­men­taires.
  • Les listes de pa­ra­mètres trop longues : au lieu de répondre di­rec­te­ment à une méthode, les objets voient leurs attributs transmis à une liste de pa­ra­mètres.
  • Les classes avec un trop grand nombre de fonctions : les classes com­pre­nant de trop nom­breuses fonctions définies comme méthodes, également connues sous le nom d’objet dieu, qui rendent presque im­pos­sible toute mo­di­fi­ca­tion du logiciel.
  • Les classes avec trop peu de fonctions : les classes com­pre­nant trop peu de fonctions définies comme méthodes, qui sont su­per­flues.
  • Les codes trop généraux avec des cas spé­ci­fiques : les fonctions avec des cas par­ti­cu­liers trop spé­ci­fiques, qui ne se pro­dui­sent que rarement ou jamais et com­pli­quent l’ajout de com­plé­ments né­ces­saires.
  • Les Middle Men : une classe distincte fait office d’in­ter­mé­diaire entre les méthodes et dif­fé­rentes classes, alors que les méthodes pour­raient appeler di­rec­te­ment une classe.

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

Le re­fac­to­ring 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 mo­di­fi­ca­tions apportées au code avec des processus de dé­ve­lop­pe­ment de logiciels, comme le dé­ve­lop­pe­ment piloté par les tests (TDD pour Test Driven De­ve­lop­ment) ou l’in­té­gra­tion continue (CI pour Con­ti­nuous In­te­gra­tion). En résumé, le TDD et la CI re­com­man­dent de tester con­ti­nuel­le­ment les petites sections de code nou­vel­le­ment ajoutées, que les pro­gram­meurs créent, intègrent et con­trô­lent sur le plan fonc­tion­nel par des tests au­to­ma­ti­sés et fréquents.

La règle d’or : modifier le programme de l’intérieur par petites étapes sans toucher aux fonctions ex­té­rieures. Après chaque mo­di­fi­ca­tion, lancer un cycle de test aussi au­to­ma­tisé que possible.

Quelles tech­niques existent ?

Il existe de très nom­breuses tech­niques concrètes de re­fac­to­ring. Les travaux foi­son­nants de Martin Fowler et Kent Beck en proposent un aperçu complet : « Re­fac­to­ring: Improving the Design of Existing Code ». En voici un court résumé :

Dé­ve­lop­pe­ment rouge-vert

Le dé­ve­lop­pe­ment rouge-vert est une méthode pilotée par les tests du dé­ve­lop­pe­ment 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’im­plé­men­ta­tion de la nouvelle fonction dans le code. Le vert symbolise la section de code la plus simple possible et né­ces­saire à 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 fonc­tion­na­lité. Le dé­ve­lop­pe­ment rouge-vert est un pilier du re­fac­to­ring en continu dans le cadre d’un dé­ve­lop­pe­ment logiciel également en continu.

Branching-by-Abs­trac­tion

Cette méthode de réusinage du code décrit une mo­di­fi­ca­tion d’un système par étapes et l’adap­ta­tion d’anciennes lignes de code im­plé­men­tées aux nouvelles sections de code. La technique Branching-by-Abs­trac­tion est gé­né­ra­le­ment utilisée lors de grandes mo­di­fi­ca­tions portant sur la hié­rar­chie des classes, l’hérédité ou l’ex­trac­tion. L’im­plé­men­ta­tion d’une abs­trac­tion qui reste liée à une ancienne im­plé­men­ta­tion permet de relier d’autres méthodes et classes à l’abs­trac­tion et de remplacer la fonc­tion­na­lité de l’ancienne section de code par cette même abs­trac­tion.

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’abs­trac­tion, tout en orientant les liens vers celle-ci. Ainsi, on peut rediriger une classe in­fé­rieure vers une classe su­pé­rieure (pull-up), ou bien les com­po­sants d’une classe su­pé­rieure vers une classe in­fé­rieure (push-down).

On peut enfin supprimer les anciennes fonctions sans prendre de risque pour la fonc­tion­na­lité de l’ensemble. Grâce à ces petites mo­di­fi­ca­tions, le fonc­tion­ne­ment 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 re­fac­to­ring doit rendre aussi lisibles que possible les méthodes du code. Dans le meilleur des cas, dès la lecture, même un pro­gram­meur extérieur doit être capable de com­prendre la logique interne d’une méthode. Pour compiler ef­fi­ca­ce­ment les méthodes, le réusinage du code propose dif­fé­rentes tech­niques. L’objectif de chaque mo­di­fi­ca­tion est de dis­tin­guer chaque méthode, de supprimer les doublons et de diviser les méthodes les plus longues en éléments distincts pour permettre leur mo­di­fi­ca­tion ul­té­rieure.

Voici des exemples de tech­niques adéquates :

  • Extraire les méthodes
  • Ajouter inline à la méthode
  • Supprimer les variables tem­po­raires
  • Remplacer les variables tem­po­raires par la méthode des requêtes
  • Insérer des variables de des­crip­tion
  • Dissocier les variables tem­po­raires
  • Supprimer les attributs des variables de paramètre
  • Remplacer une méthode par une méthode-objet
  • Remplacer l’al­go­rithme

Décaler les pro­prié­tés entre dif­fé­rentes classes

Pour améliorer un code, on doit parfois décaler les attributs ou méthodes d’une classe à l’autre. Voici les tech­niques 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 in­ter­mé­diaire
  • Insérer une méthode externe
  • Insérer une extension locale

Or­ga­ni­sa­tion 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 fonc­tion­ne­ment du logiciel dès la moindre mo­di­fi­ca­tion, doivent être supprimés et répartis entre les classes adéquates.

Exemples de tech­niques adaptées :

  • En­cap­su­ler 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 ob­ser­vables
  • En­cap­su­ler les attributs
  • Remplacer un ensemble de données par une classe de données

Sim­pli­fier les ex­pres­sions con­di­tion­nelles

Au cours du re­fac­to­ring, il faut, autant que possible, sim­pli­fier les ex­pres­sions con­di­tion­nelles. Exemple des tech­niques adaptées :

  • Scinder les relations
  • Fusionner les ex­pres­sions con­di­tion­nelles
  • Fusionner les ins­truc­tions répétées dans les ex­pres­sions con­di­tion­nelles
  • Supprimer les boutons de contrôle
  • Remplacer les con­di­tions im­bri­quées par des gardiens
  • Remplacer les dis­tinc­tions par des po­ly­mor­phismes
  • Insérer des objets nuls

Sim­pli­fier l’in­vo­ca­tion des méthodes

L’in­vo­ca­tion des méthodes gagne en rapidité et en sim­pli­cité grâce aux tech­niques suivantes, entre autres :

  • Renommer les méthodes
  • Ajouter des pa­ra­mètres
  • Supprimer des pa­ra­mètres
  • Remplacer des pa­ra­mètres par des méthodes ex­pli­cites
  • Remplacer le code erroné par des ex­cep­tions

Exemple de re­fac­to­ring : 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 clai­re­ment et ra­pi­de­ment sur sa fonc­tion­na­lité. La méthode doit retourner le code postal contenu dans l’adresse d’un bureau, mais le code ne le reflète pas clai­re­ment. Pour gagner en précision dans la for­mu­la­tion, 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());

Re­fac­to­ring : quels avantages, quels in­con­vé­nients ?

Avantages In­con­vé­nients
Une meilleure com­pré­hen­sion facilite la main­te­nance et les pos­si­bi­li­tés d’extension du logiciel. Un manque de précision lors du re­fac­to­ring peut im­plé­men­ter de nouveaux bugs et de nouvelles erreurs dans le code.
La res­truc­tu­ra­tion du code source peut se faire sans modifier le fonc­tion­ne­ment. Il n’existe pas de dé­fi­ni­tion précise de ce qu’est un « code propre ».
Une meilleure li­si­bi­lité accroît la com­pré­hen­sion du code pour les autres pro­gram­meurs. Bien souvent, le client ne voit pas la dif­fé­rence lorsque le code est amélioré, car le fonc­tion­ne­ment reste identique : le gain n’est donc pas évident.
Retirer les re­don­dances et les doublons améliore l’ef­fi­ca­cité du code. Dans de grandes équipes tra­vail­lant sur le réusinage du code, la coor­di­na­tion peut exiger un temps éton­nam­ment long.
Des méthodes in­dé­pen­dantes évitent que les mo­di­fi­ca­tions locales aient un impact sur une autre partie du code.
Un code propre, avec des méthodes et des classes courtes et in­dé­pen­dantes, se ca­rac­té­rise par une plus grande facilité de test.

Le principe de base du re­fac­to­ring est le suivant : ajouter de nouvelles fonctions uni­que­ment si celles-ci ne modifient pas le com­por­te­ment initial du code source. On peut apporter des mo­di­fi­ca­tions au code source (re­fac­to­ring) uni­que­ment si celles-ci ne créent pas de nouvelle fonction.

Aller au menu principal