Accéder à l'en-tête Accéder au contenu principal Accéder au pied de page
Retour aux actualités
Bonnes pratiques de dév
11/11/2018 Yvan Phelizot

Strategic refactoring

Et voilà, vous arrivez sur une nouvelle mission. Vous commencez à prendre vos marques, installer votre environnement. Vous créez une copie du code sur votre poste et commencez à le parcourir. A ce moment-là, vous vous rappelez qu’on vous avait promis un challenge. Et quelle challenge, c’est un des douze travaux d’Hercule, ce serait même les écuries d’Augias. Le code est “sale”. Vous comprenez pourquoi il leur a été  impossible d’aller plus vite depuis plusieurs mois. Le code semble si “fragile”, le moindre ajout d’une fonctionnalité est un exercice périlleux. Mais, tel le héros grec, vous ne vous laissez pas abattre par l’adversité et vous vous préparez à tous laver à grandes eaux.

La réaction classique face à un tel logiciel serait de tout ré-écrire. Mais il ne s’agit pas d’une solution pérenne. Il faut considérer:

  • Le temps pour y arriver
  • Le risque de faire disparaître des fonctionnalités
  • Le fait que rien n’a changé. Pourquoi la nouvelle version ne terminerait pas dans le même état que son ancêtre après tout?

Le refactoring s’annonce difficile, quelle approche avoir? Cet article va vous présenter un ensemble de techniques et de stratégies pour résoudre ce problème!

Le but d’un refactoring est de changer le forme sans changer la forme, c’est-à-dire améliorer la manière dont l’application est écrite tout en restant iso-fonctionnelle. Première chose à faire sur un projet: on regarde le code et on évalue les dégâts. Après tout, ce n’est pas toujours si grave que ça. Il va être nécessaire de détecter les “smells” ou mauvaises pratiques que l’on trouve dans le code. Mauvais nommage, code compliqué, …

Puis, nous allons chercher à réduire la duplication et à rendre le code plus compréhensible, plus explicite sur les concepts métier qu’il sous-tend. Enfin, nous chercherons à réduire au maximum le code. Moins de code, moins de bugs!

Ensuite, il va être nécessaire de déterminer la partie de code que nous souhaitons retravailler. Il peut s’agir des classes et fonctions impactées par la nécessite d’ajouter une nouvelle fonctionnalité. Il est aussi possible de décider de retravailler certains blocs afin de réduire la dette technique. Des outils existent pour vous aider dans cette tâche comme Sonar qui va nous aider à trouver le code le plus complexe que l’on va vouloir simplifier.

Soit, nous avons notre objectif en vue. Maintenant, première question à se poser: existe-t’il des tests? Ils seront notre filet de sécurité. Ils seront là pour nous avertir cas de d’altération des fonctionnalités. Ils serviront aussi de documentation afin de garder une trace des fonctionnalités de l’application? Et s’il n’y en a pas? Il va être nécessaire d’en écrire. Mais, pas n’importe comment! Nous allons utiliser plusieurs métriques et outils pour nous aider. Tout d’abord la complexité cyclomatique nous donner une limite basse sur le nombre de tests pour atteindre une couverture de code à 100%. Ensuite, nous pouvons utiliser la couverture de branche. Cependant, cela donne un nombre exponentiel. Nous pouvons utiliser aussi le mutation testing. Cet outil sert à “tester les tests”. Il modifie le code de production et exécute les tests. Si aucun test échoue, c’est que les tests ne servent à rien ou que les cas ne sont pas correctement couverts (cas au limite, …).

Et si on a des tests? On les regarde aussi! On peut déjà voir s’ils jouent bien leur rôle de documentation et s’ils aident à comprendre le fonctionnel. Ensuite, quelle est la couverture de code? Après tout, avoir des tests. . Enfin, dernier truc, les assertions sont-elles bien utilisées? Il m’est déjà arrivé de trouver un projet sans assert ou avec des assertTrue(true).

Une fois que nous avons notre ceinture de sécurité, différentes stratégies s’offrent à nous, de la plus simple à la plus compliquée:

  • Refactoring automatique: on peut aussi s’appuyer sur l’IDE qui nous indique les refactorings automatiques possibles.
  • Quick win: on va chercher les petites actions sans risque qui aident. Un renommage de variables, simplifier une fonction, …
  • Rendre « statique ». L’idée est que si une fonction est statique, elle représente sans doute un concept particulier. On pourra alors l’isoler et la tester.
  • Outil de refactoring : on s’appuiera autant que possible sur les outils fournis par l’IDE pour les opérations comme le renommage, l’extraction de méthode, … Par exemple, le constructeur suivant :
constructor(d1: number, d2: number, d3: number, d4: number, _5: number)

devient:

constructor(d1: number, d2: number, d3: number, d4: number, d5: number)
  • Clarifier les concepts: on va chercher à améliorer le nommage, à ajouter des types, définir une algèbre de types, … pour rendre le plus explicite possible les notions. Le constructeur va de:
static chance(d1: number, d2: number, d3: number, d4: number, d5: number): number

Vers

type FACE = 1 | 2 | 3 | 4 | 5 | 6;

static chance(d1: FACE, d2: FACE, d3: FACE, d4: FACE, d5: FACE): RESULTAT
  • From scratch : face à une fonction complexe, il est parfois nécessaire de repartir de zéro.
  • Méthode Mikado : face à des refactorings qui peuvent demander plusieurs jours ou plusieurs semaines, on peut s’appuyer sur la méthode Mikado pour ne pas perdre le résultat de vue. Le principe est simple : on identifie un résultat voulu (par exemple, rendre une fonction statique), on effectue une action simple (on la rend non statique). Si les tests passent, on continue. Sinon, on revient en arrière et on note les pré-requis à satisfaire.

Enfin, refactorer le code, c’est aussi refactorer les tests. Certains tests sont parfois trop gros, pas clairs, … Un travail important est nécessaire pour décomposer les tests en cas de gestion unitaire. Dans l’exemple suivant, trois règles de gestion sont présentes.

describe('One pair', () => {

it('scores the sum of the highest pair', () => {

   assert.equal(6,  Yatzy.score_pair(3, 4, 3, 5, 6));

   assert.equal(10, Yatzy.score_pair(5, 3, 3, 3, 5));

   assert.equal(12, Yatzy.score_pair(5, 3, 6, 6, 5));

 });

});

On transformera donc le test en trois tests explicites:

describe('One pair', () => {

it('scores the sum of the pair', () => {

  assert.equal(6,  Yatzy.score_pair(3, 4, 3, 5, 6));

});

it('scores the sum of the pair and ignore other patterns', () => {

  assert.equal(10, Yatzy.score_pair(5, 3, 3, 3, 5));

});

it('scores the sum of the highest pair', () => {

  assert.equal(12, Yatzy.score_pair(5, 3, 6, 6, 5));

});

});

Vous voilà donc prêt à relever les différents défis, tel notre héros grec! Il vous faudra être patient, car cela demande beaucoup de temps pour construire un logiciel de qualité. Et il vous vaudra aussi beaucoup de courage pour ne pas échouer face à l’adversité!