Pourquoi apprendre le refactoring ?

Quand mon IDE fait déjà le job pour moi 🤔


Features https://monkeyuser.com

Voici un exemple de conversation tirée de mon expérience:

[Développeur] - Comment renommer une colonne de notre table sans rien casser ?

[Moi] - Est-ce que tu sais comment renommer une variable membre de classe sans rien casser ?

[Développeur] - Ben vi, j’utilise IntelliJ ?

[Moi] - …

C’est exactement pour ce type de situation qu’il est très utile de connaître les mécanismes de refactoring décrits dans le bouquin de Martin Fowler même s’il existe des outils qui les automatisent: les connaître améliore notre capacité à raisonner et à les adapter dans des contextes différents.

On va prendre un exemple tout simple pour illustrer mon propos:

class Employee {
  private readonly _salary: number

  constructor(salary: number) {
    this._salary = salary
  }

  get salary() {
    return this._salary
  }
}

Notre objectif est de renommer la propriété salary en netSalary.

[Développeur] - Ah, mais c’est facile: dans IntelliJ, je te fais ça en 3 secondes.

[Moi] - Oui, tu as raison et j’utiliserais évidemment IntelliJ comme toi pour faire le job. Mais apprenons plutôt à faire la même chose sans utiliser IntelliJ et voir si on peut utiliser cette connaissance dans d’autres contextes. Est-ce que tu te rappelles de ce qu’est un refactoring d’abord ?

[Développeur] - Bien sûr: c’est quand on veut changer la structure du code sans en changer le comportement.

[Moi] - Ouah, tu m’impressionnes dis-donc. Et comment s’assure-t-on que le comportement n’a pas changé ?

[Développeur] - Grâce à nos tests!!!

[Moi] - C’est exact. Pour être plus précis, un refactoring peut être défini par une série de petites étapes très simples, voire simplistes, chacune changeant l’état du code et, idéalement, sans que les tests n’échouent entre chaque étape.

[Développeur] - Ah mais moi, j’aime bien faire pleins de grosses modifications en même temps quand je fais du refactoring, ça va beaucoup plus vite!

[Moi] - Tu peux mais ce n’est plus vraiment du refactoring, cela ressemble plutôt à ce que Sandi Metz appelle du “random rehacktoring”. Si tu fais beaucoup de modifications d’un coup et qu’une erreur se produit, il peut être plus difficile de trouver la raison de cette erreur.

[Développeur] - Donc si j’y vais par petites étapes, j’irai plus lentement mais ce sera plus sûr, c’est ça ?

[Moi] - Oui au début…et avec l’expérience, tu te rendras compte que cette sécurité te permettra d’avancer plus vite en pratique car tu feras beaucoup moins d’erreurs. Bon, revenons à notre exemple, nous allons voir comment renommer ensemble une variable membre de classe par petites étapes.

  1. Créer une nouvelle propriété netSalary
class Employee {
  private readonly _salary: number
  private readonly _netSalary?: number

  constructor(salary: number) {
    this._salary = salary
  }

  get salary() {
    return this._salary
  }
}

La nouvelle variable a été déclarée optionnelle pour que le code puisse compiler.

  1. Alimenter la nouvelle propriété avec la valeur de l’ancienne
class Employee {
  private readonly _salary: number
  private readonly _netSalary: number

  constructor(salary: number) {
    this._salary = salary
    this._netSalary = salary
  }

  get salary() {
    return this._salary
  }

  get netSalary() {
    return this._netSalary
  }
}

Notez que l’on conserve l’ancienne propriété pour ne rien casser.

  1. Mettre à jour tous les clients de Employee pour qu’ils utilisent la nouvelle propriété à la place de l’ancienne
// exemple simpliste de client
console.log(employee.netSalary)

au lieu de:

// exemple simpliste de client
console.log(employee.salary)

En pratique, on met à jour un client à la fois, on teste et si ça fonctionne, on passe au client suivant.

  1. Une fois tous les clients mis à jour, supprimer la propriété salary de la classe
class Employee {
  private readonly _netSalary: number

  constructor(salary: number) {
    this._netSalary = salary
  }

  get netSalary() {
    return this._netSalary
  }
}

C’est clair que ce n’est pas très rapide mais ce qui est certain, c’est que c’est très safe: les tests ne seront jamais rouge après chacune de ces étapes et on pourrait livrer l’application à tout moment si on le souhaitait.

Nous allons voir comment adapter ces mécanismes au refactoring d’une API à présent.

Une API JSON

Supposons maintenant que l’on souhaite refactorer une API et renommer un champ dans une réponse JSON.

function getSalary(req: Request, res: Response) {
  const employeeId = req.param('employeeId')
  const salary = repository.getSalary(employeeId)
  return res.json({
    salary: salary
  })
}

Notre but est ici de renommer salary en netSalary. Comment faire ce refactoring sans casser les clients de notre API ?

  1. Créer un champ netSalary dans l’API
function getSalary(req: Request, res: Response) {
  const employeeId = req.param('employeeId')
  const salary = repository.getSalary(employeeId)
  return res.json({
    salary: salary,
    netSalary: 0
  })
}
  1. Alimenter la nouvelle propriété avec la valeur de salary
function getSalary(req: Request, res: Response) {
  const employeeId = req.param('employeeId')
  const salary = repository.getSalary(employeeId)
  return res.json({
    salary: salary,
    netSalary: salary
  })
}

Notez que l’on conserve l’ancien champ pour ne rien casser.

  1. Propager l’information auprès des clients afin qu’ils utilisent netSalary au lieu de salary et attendre qu’ils soient tous mis à jour.

Bien entendu, il faut déployer l’application après l’étape 2 pour que les clients puissent le faire.

Cette étape peut prendre beaucoup de temps en fonction du nombre de clients (et parfois, ce n’est pas possible et les deux champs doivent coexister ad vitam eternam)

  1. Supprimer salary de l’API

A cette étape, les clients ont tous éte mis à jour auparavant et l’ancien champ n’est plus utilisé.

function getSalary(req: Request, res: Response) {
  const employeeId = req.param('employeeId')
  const salary = repository.getSalary(employeeId)
  return res.json({
    netSalary: salary
  })
}

Et pour une base de données ?

Supposons maintenant que l’on ait une table dont on souhaite renommer une colonne:

Colonne
id
salary

Pour faire original, on souhaite renommer la colonne salary en net_salary.

Voici une série d’étapes que l’on pourrait appliquer:

  1. Créer une nouvelle colonne net_salary
Colonne
id
salary
net_salary

Elle peut être optionnelle dans un premier temps et rendue obligatoire lorsque salary sera supprimée.

  1. Alimenter la nouvelle colonne avec un trigger qui synchronise salary avec net_salary et un script sql qui migre les valeurs pour les anciens enregistrements

  2. Mettre à jour le code pour utiliser la nouvelle colonne

  • Les opérations de lecture
connection.query('select net_salary from employees where id=?', [id], (err, results) => {
  // handle error
  const salary = results.net_salary
});
  • Les opérations d’écriture
connection.query('update employees set net_salary=? where id=?', [salary, id], (err, results) => {
  // handle error
});

Ici aussi, on change une opération à la fois et on teste après chaque changement pour gagner en confiance.

  1. (a) Une fois les clients mis à jour, supprimer les références à la colonne salary dans le code:
connection.query('update employees set net_salary=? where id=?', [salary, salary, id], (err) => { 
  // handle error
});
  1. (b) Supprimer enfin le trigger et la colonne
Colonne
id
net_salary

Dans cet exemple également, il est possible de livrer le code de façon safe après chacune des étapes.

Conclusion

Si on récapitule les grandes étapes de ces refactorings dans un tableau:

Renommer propriété de classe Renommer champ api Renommer colonne table
créer une propriété créer un champ créer une nouvelle colonne
alimenter la nouvelle propriété alimenter le nouveau champ alimenter la nouvelle colonne
mettre à jour les clients mettre à jour les clients mettre à jour les clients
supprimer la propriété supprimer le champ supprimer les références à la colonne

Il y a quand même pas mal de similitudes, n’est-ce pas ?

Alors, oui, mes exemples sont très simples (je ne parle pas des contraintes ou des clés étrangères dans celui de la base de données) mais l’idée générale resterait la même.

Connaître les mécanismes de refactoring nous aide à mieux raisonner dans des situations plus compliquées, et c’est bien pour cela que je relis souvent le livre de Martin Fowler pour les pratiquer et les assimiler…sans IntelliJ !


© 2023 Du côté de chez Fouad. Tous droits réservés.