Maison > interface Web > js tutoriel > Laver votre code : diviser pour régner, ou fusionner et se détendre

Laver votre code : diviser pour régner, ou fusionner et se détendre

Linda Hamilton
Libérer: 2024-11-11 19:30:03
original
995 Les gens l'ont consulté

Washing your code: divide and conquer, or merge and relax

Vous lisez un extrait de mon livre sur le code propre, « Washing your code ». Disponible au format PDF, EPUB et en édition papier et Kindle. Obtenez votre exemplaire maintenant.


Savoir comment organiser le code en modules ou en fonctions, et quand le bon moment est d'introduire une abstraction au lieu de dupliquer le code, est une compétence importante. Écrire du code générique que d’autres peuvent utiliser efficacement est encore une autre compétence. Il existe autant de raisons de diviser le code que de le conserver ensemble. Dans ce chapitre, nous aborderons certaines de ces raisons.

Laissons les abstractions grandir

Nous, développeurs, détestons faire deux fois le même travail. DRY est un mantra pour beaucoup. Cependant, lorsque nous avons deux ou trois morceaux de code qui font en quelque sorte la même chose, il est peut-être encore trop tôt pour introduire une abstraction, aussi tentante que cela puisse paraître.

Info : Le principe Ne vous répétez pas (DRY) exige que « chaque élément de connaissance doit avoir une représentation unique, sans ambiguïté et faisant autorité au sein d'un système », ce qui est souvent interprété comme toute duplication de code est strictement verboten.

Vivez avec la douleur de la duplication de code pendant un certain temps ; ce n’est peut-être pas si mal au final, et le code n’est en fait pas exactement le même. Un certain niveau de duplication de code est sain et nous permet d'itérer et de faire évoluer le code plus rapidement sans craindre de casser quelque chose.

Il est également difficile de proposer une bonne API lorsque l’on ne considère que quelques cas d’utilisation.

Gérer du code partagé dans de grands projets avec de nombreux développeurs et équipes est difficile. Les nouvelles exigences pour une équipe peuvent ne pas fonctionner pour une autre équipe et briser leur code, ou nous nous retrouvons avec un monstre spaghetti impossible à maintenir avec des dizaines de conditions.

Imaginez que l'équipe A ajoute un formulaire de commentaire à sa page : un nom, un message et un bouton d'envoi. Ensuite, l’équipe B a besoin d’un formulaire de commentaires pour trouver le composant de l’équipe A et essayer de le réutiliser. Ensuite, l'équipe A veut également un champ de courrier électronique, mais elle ne sait pas que l'équipe B utilise son composant, elle ajoute donc un champ de courrier électronique obligatoire et interrompt la fonctionnalité pour les utilisateurs de l'équipe B. Ensuite, l'équipe B a besoin d'un champ de numéro de téléphone, mais elle sait que l'équipe A utilise le composant sans celui-ci, elle ajoute donc une option pour afficher un champ de numéro de téléphone. Un an plus tard, les deux équipes se détestent pour avoir brisé le code de l'autre, et le composant est plein de conditions et impossible à maintenir. Les deux équipes gagneraient beaucoup de temps et auraient des relations plus saines entre elles si elles maintenaient des composants séparés composés de composants partagés de niveau inférieur, comme un champ de saisie ou un bouton.

Conseil : Cela pourrait être une bonne idée d'interdire à d'autres équipes d'utiliser notre code à moins qu'il ne soit conçu et marqué comme partagé. Le Dependency Cruiser est un outil qui pourrait aider à établir de telles règles.

Parfois, nous devons faire reculer une abstraction. Lorsque nous commençons à ajouter des conditions et des options, nous devons nous demander : s’agit-il toujours d’une variation de la même chose ou d’une nouvelle chose qui devrait être séparée ? Ajouter trop de conditions et de paramètres à un module peut rendre l'API difficile à utiliser et le code difficile à maintenir et à tester.

La duplication est moins chère et plus saine qu'une mauvaise abstraction.

Info : Voir l'article de Sandi Metz, The Wrong Abstraction, pour une excellente explication.

Plus le niveau du code est élevé, plus nous devons attendre avant de l'abstraire. Les abstractions d'utilitaires de bas niveau sont beaucoup plus évidentes et stables que la logique métier.

La taille n'a pas toujours d'importance

La réutilisation du code n'est pas la seule, ni même la raison la plus importante, pour extraire un morceau de code dans une fonction ou un module distinct.

La

La longueur du code est souvent utilisée comme mesure pour savoir quand nous devons diviser un module ou une fonction, mais la taille à elle seule ne rend pas le code difficile à lire ou à maintenir.

Découper un algorithme linéaire, même long, en plusieurs fonctions puis les appeler les unes après les autres rend rarement le code plus lisible. Sauter entre les fonctions (et plus encore entre les fichiers) est plus difficile que faire défiler, et si nous devons examiner l'implémentation de chaque fonction pour comprendre le code, alors l'abstraction n'était pas la bonne.

Info : Egon Elbre a écrit un bel article sur la psychologie de la lisibilité du code.

Voici un exemple, adapté du blog Google Testing :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

J'ai tellement de questions sur l'API de la classe Pizza, mais voyons quelles améliorations les auteurs suggèrent :

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Ce qui était déjà complexe et alambiqué est maintenant encore plus complexe et alambiqué, et la moitié du code n'est constituée que d'appels de fonction. Cela ne rend pas le code plus facile à comprendre, mais cela rend son utilisation presque impossible. L'article ne montre pas le code complet de la version refactorisée, peut-être pour rendre le propos plus convaincant.

Pierre « catwell » Chapuis suggère dans son article de blog d'ajouter des commentaires au lieu de nouvelles fonctions :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

C'est déjà bien mieux que la version split. Une solution encore meilleure consisterait à améliorer les API et à rendre le code plus clair. Pierre suggère que le préchauffage du four ne devrait pas faire partie de la fonction createPizza() (et cuire moi-même de nombreuses pizzas, je suis tout à fait d'accord !) car dans la vraie vie, le four est déjà là et probablement déjà chaud à cause de la pizza précédente. Pierre suggère également que la fonction devrait renvoyer la boîte, pas la pizza, car dans le code original, la boîte disparaît en quelque sorte après toute la magie du tranchage et de l'emballage, et nous nous retrouvons avec la pizza tranchée dans nos mains.

Il existe de nombreuses façons de cuisiner une pizza, tout comme il existe de nombreuses façons de coder un problème. Le résultat peut sembler identique, mais certaines solutions sont plus faciles à comprendre, à modifier, à réutiliser et à supprimer que d'autres.

La dénomination peut également être un problème lorsque toutes les fonctions extraites font partie du même algorithme. Nous devons inventer des noms plus clairs que le code et plus courts que les commentaires – ce n’est pas une tâche facile.

Info : Nous parlons de commenter le code dans le chapitre Éviter les commentaires, et de nommer dans le chapitre Naming is hard.

Vous ne trouverez probablement pas beaucoup de petites fonctions dans mon code. D'après mon expérience, les raisons les plus utiles pour diviser le code sont la fréquence de changement et la raison du changement.

Code séparé qui change souvent

Commençons par changer la fréquence. La logique métier change beaucoup plus souvent que les fonctions utilitaires. Il est logique de séparer le code qui change souvent du code qui est très stable.

Le formulaire de commentaire dont nous avons parlé plus tôt dans ce chapitre est un exemple du premier ; une fonction qui convertit les chaînes camelCase en kebab-case est un exemple de cette dernière. Le formulaire de commentaires est susceptible de changer et de diverger au fil du temps lorsque de nouvelles exigences commerciales surviennent ; Il est peu probable que la fonction de conversion de cas change et elle peut être réutilisée en toute sécurité dans de nombreux endroits.

Imaginez que nous créons un joli tableau pour afficher des données. Nous pensons peut-être que nous n'aurons plus jamais besoin de cette conception de table, nous décidons donc de conserver tout le code de la table dans un seul module.

Au sprint suivant, nous obtenons une tâche pour ajouter une autre colonne au tableau, nous copions donc le code d'une colonne existante et y modifions quelques lignes. Au prochain sprint, nous devons ajouter une autre table avec le même design. Au prochain sprint, il faudra changer le design des tables…

Notre module de table a au moins trois raisons de changer, ou responsabilités :

  • de nouvelles exigences commerciales, comme une nouvelle colonne de tableau ;
  • Changements d'interface utilisateur ou de comportement, comme l'ajout d'un tri ou le redimensionnement des colonnes ;
  • modifications de conception, comme le remplacement des bordures par des arrière-plans de lignes rayées.

Cela rend le module plus difficile à comprendre et plus difficile à modifier. Le code de présentation ajoute beaucoup de verbosité, ce qui rend plus difficile la compréhension de la logique métier. Pour modifier l'une des responsabilités, nous devons lire et modifier davantage de code. Cela rend l'itération plus difficile et plus lente sur l'un ou l'autre.

Avoir une table générique en tant que module séparé résout ce problème. Désormais, pour ajouter une autre colonne à un tableau, il suffit de comprendre et de modifier l'un des deux modules. Nous n'avons pas besoin de savoir quoi que ce soit sur le module de table générique, à l'exception de son API publique. Pour modifier la conception de toutes les tables, il nous suffit de modifier le code du module de table générique et nous n'avons probablement pas besoin de toucher aux tables individuelles.

Cependant, selon la complexité du problème, il est correct, et souvent préférable, de commencer par une approche monolithique et d'en extraire une abstraction plus tard.

Même la réutilisation du code peut être une raison valable pour séparer le code : si nous utilisons un composant sur une page, nous en aurons probablement bientôt besoin sur une autre page.

Gardez ensemble le code qui change en même temps

Il pourrait être tentant d'extraire chaque fonction dans son propre module. Cependant, cela présente aussi des inconvénients :

  • D'autres développeurs peuvent penser qu'ils peuvent réutiliser la fonction ailleurs, mais en réalité, cette fonction n'est probablement pas suffisamment générique ou testée pour être réutilisée.
  • La création, l'importation et le basculement entre plusieurs fichiers créent une surcharge inutile lorsque la fonction n'est utilisée qu'à un seul endroit.
  • Ces fonctions restent souvent dans la base de code longtemps après la disparition du code qui les utilisait.

Je préfère garder les petites fonctions qui ne sont utilisées que dans un seul module au début du module. De cette façon, nous n'avons pas besoin de les importer pour les utiliser dans le même module, mais les réutiliser ailleurs serait gênant.

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Dans le code ci-dessus, nous avons un composant (FormattedAddress) et une fonction (getMapLink()) qui ne sont utilisés que dans ce module, ils sont donc définis en haut du fichier.

Si nous devons tester ces fonctions (et nous devrions le faire !), nous pouvons les exporter depuis le module et les tester avec la fonction principale du module.

Il en va de même pour les fonctions destinées à être utilisées uniquement avec une certaine fonction ou un certain composant. Les conserver dans le même module montre plus clairement que toutes les fonctions vont ensemble et rend ces fonctions plus visibles.

Un autre avantage est que lorsque nous supprimons un module, nous supprimons automatiquement ses dépendances. Le code des modules partagés reste souvent pour toujours dans la base de code car il est difficile de savoir s'il est toujours utilisé (bien que TypeScript facilite cela).

Info : De tels modules sont parfois appelés modules profonds : des modules relativement volumineux qui encapsulent des problèmes complexes mais possèdent des API simples. Le contraire des modules profonds sont les modules peu profonds : de nombreux petits modules qui doivent interagir les uns avec les autres.

Si l'on doit souvent modifier plusieurs modules ou fonctions en même temps, il serait peut-être préférable de les fusionner en un seul module ou fonction. Cette approche est parfois appelée colocation.

Voici quelques exemples de colocation :

  • Composants React : conserver tout ce dont un composant a besoin dans le même fichier, y compris le balisage (JSX), les styles (CSS dans JS) et la logique, plutôt que de séparer chacun dans son propre fichier, probablement dans un dossier séparé.
  • Tests : conserver les tests à côté du fichier du module plutôt que dans un dossier séparé.
  • Convention Ducks pour Redux : conservez les actions, les créateurs d'actions et les réducteurs associés dans le même fichier plutôt que de les avoir dans trois fichiers dans des dossiers séparés.

Voici comment l'arborescence des fichiers change avec la colocation :

Séparé Colocalisé
Separated Colocated
React components
src/components/Button.tsx src/components/Button.tsx
styles/Button.css
Tests
src/util/formatDate.ts src/util/formatDate.ts
tests/formatDate.ts src/util/formatDate.test.ts
Ducks
src/actions/feature.js src/ducks/feature.js
src/actionCreators/feature.js
src/reducers/feature.js
Composants React<🎜> src/components/Button.tsx src/components/Button.tsx styles/Bouton.css <🎜>Tests<🎜> src/util/formatDate.ts src/util/formatDate.ts tests/formatDate.ts src/util/formatDate.test.ts <🎜>Canards<🎜> src/actions/feature.js src/ducks/feature.js src/actionCreators/feature.js src/reducers/feature.js

Info : Pour en savoir plus sur la colocation, lisez l'article de Kent C. Dodds.

Une plainte courante concernant la colocation est qu'elle rend les composants trop volumineux. Dans de tels cas, il est préférable d'extraire certaines parties dans leurs propres composants, ainsi que le balisage, les styles et la logique.

L'idée de colocation entre également en conflit avec la séparation des préoccupations — une idée dépassée qui a conduit les développeurs Web à conserver HTML, CSS et JavaScript dans des fichiers séparés (et souvent dans des parties distinctes de l'arborescence des fichiers) pour trop long, nous obligeant à modifier trois fichiers en même temps pour apporter les modifications les plus élémentaires aux pages Web.

Info : La raison du changement est également connue sous le nom de principe de responsabilité unique, qui stipule que « chaque module, classe ou fonction doit avoir la responsabilité d'une seule partie de la fonctionnalité. fourni par le logiciel, et cette responsabilité doit être entièrement assumée par la classe. »

Balayez ce vilain code sous le tapis

Parfois, nous devons travailler avec une API particulièrement difficile à utiliser ou sujette aux erreurs. Par exemple, cela nécessite plusieurs étapes dans un ordre spécifique ou l’appel d’une fonction avec plusieurs paramètres toujours identiques. C'est une bonne raison de créer une fonction utilitaire pour nous assurer que nous faisons toujours les choses correctement. En bonus, nous pouvons désormais écrire des tests pour ce bout de code.

Les manipulations de chaînes, telles que les URL, les noms de fichiers, la conversion de casse ou le formatage, sont de bons candidats pour l'abstraction. Très probablement, il existe déjà une bibliothèque pour ce que nous essayons de faire.

Considérez cet exemple :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Il faut un certain temps pour se rendre compte que ce code supprime l'extension du fichier et renvoie le nom de base. Non seulement c'est inutile et difficile à lire, mais cela suppose également que l'extension comporte toujours trois caractères, ce qui pourrait ne pas être le cas.

Réécrivons-le à l'aide d'une bibliothèque, le module path intégré de Node.js :

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Maintenant, ce qui se passe est clair, il n’y a pas de chiffres magiques et cela fonctionne avec des extensions de fichiers de n’importe quelle longueur.

Les autres candidats à l'abstraction incluent les dates, les capacités de l'appareil, les formulaires, la validation des données, l'internationalisation, etc. Je recommande de rechercher une bibliothèque existante avant d'écrire une nouvelle fonction utilitaire. Nous sous-estimons souvent la complexité de fonctions apparemment simples.

Voici quelques exemples de telles bibliothèques :

  • Lodash : fonctions utilitaires en tout genre.
  • Date-fns : fonctions pour travailler avec les dates, telles que l'analyse, la manipulation et le formatage.
  • Zod : validation de schéma pour TypeScript.

Bénis le refactoring en ligne !

Parfois, on se laisse emporter et créons des abstractions qui ne simplifient ni le code ni ne le raccourcissent :

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Autre exemple :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

La meilleure chose que nous puissions faire dans de tels cas est d'appliquer le tout-puissant refactoring en ligne : remplacer chaque appel de fonction par son corps. Pas d'abstraction, pas de problème.

Le premier exemple devient :

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Et le deuxième exemple devient :

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Le résultat n'est pas seulement plus court et plus lisible ; désormais, les lecteurs n'ont plus besoin de deviner ce que font ces fonctions, car nous utilisons désormais des fonctions et fonctionnalités natives JavaScript sans abstractions maison.

Dans de nombreux cas, un peu de répétition fait du bien. Prenons cet exemple :

function FormattedAddress({ address, city, country, district, zip }) {
  return [address, zip, district, city, country]
    .filter(Boolean)
    .join(', ');
}

function getMapLink({ name, address, city, country, zip }) {
  return `https://www.google.com/maps/?q=${encodeURIComponent(
    [name, address, zip, city, country].filter(Boolean).join(', ')
  )}`;
}

function ShopsPage({ url, title, shops }) {
  return (
    <PageWithTitle url={url} title={title}>
      <Stack as="ul" gap="l">
        {shops.map(shop => (
          <Stack key={shop.name} as="li" gap="m">
            <Heading level={2}>
              <Link href={shop.url}>{shop.name}</Link>
            </Heading>
            {shop.address && (
              <Text variant="small">
                <Link href={getMapLink(shop)}>
                  <FormattedAddress {...shop} />
                </Link>
              </Text>
            )}
          </Stack>
        ))}
      </Stack>
    </PageWithTitle>
  );
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cela semble parfaitement bien et ne soulèvera aucune question lors de la révision du code. Cependant, lorsque nous essayons d'utiliser ces valeurs, la saisie semi-automatique affiche uniquement le nombre au lieu des valeurs réelles (voir une illustration). Cela rend plus difficile le choix de la bonne valeur.

Washing your code: divide and conquer, or merge and relax

Nous pourrions intégrer la constante baseSpacing :

const file = 'pizza.jpg';
const prefix = file.slice(0, -4);
// → 'pizza'
Copier après la connexion
Copier après la connexion

Maintenant, nous avons moins de code, c'est tout aussi simple à comprendre, et la saisie semi-automatique montre les valeurs réelles (voir l'illustration). Et je ne pense pas que ce code changera souvent – ​​probablement jamais.

Washing your code: divide and conquer, or merge and relax

Séparez « quoi » et « comment »

Considérez cet extrait d'une fonction de validation de formulaire :

const file = 'pizza.jpg';
const prefix = path.parse(file).name;
// → 'pizza'
Copier après la connexion
Copier après la connexion

Il est assez difficile de comprendre ce qui se passe ici : la logique de validation se mêle aux messages d’erreur, de nombreuses vérifications sont répétées…

Nous pouvons diviser cette fonction en plusieurs parties, chacune responsable d'une seule chose :

  • une liste de validations pour un formulaire particulier ;
  • une collection de fonctions de validation, telles que isEmail();
  • une fonction qui valide toutes les valeurs du formulaire à l'aide d'une liste de validations.

Nous pouvons décrire les validations de manière déclarative sous forme de tableau :

// my_feature_util.js
const noop = () => {};

export const Utility = {
  noop
  // Many more functions…
};

// MyComponent.js
function MyComponent({ onClick }) {
  return <button onClick={onClick}>Hola!</button>;
}

MyComponent.defaultProps = {
  onClick: Utility.noop
};
Copier après la connexion
Copier après la connexion

Chaque fonction de validation et la fonction qui exécute les validations sont assez génériques, nous pouvons donc soit les faire abstraction, soit utiliser une bibliothèque tierce.

Maintenant, nous pouvons ajouter une validation pour n'importe quel formulaire en décrivant quels champs nécessitent quelles validations et quelle erreur afficher lorsqu'une certaine vérification échoue.

Info : Voir le chapitre Conditions à éviter pour le code complet et une explication plus détaillée de cet exemple.

J'appelle ce processus séparation du « quoi » et du « comment » :

  • le « quoi » sont les données — la liste des validations pour un formulaire particulier ;
  • le « comment » sont les algorithmes — les fonctions de validation et la fonction d'exécuteur de validation.

Les avantages sont :

  • Lisibilité :souvent, nous pouvons définir le « quoi » de manière déclarative, en utilisant des structures de données de base telles que des tableaux et des objets.
  • Maintenabilité : nous changeons le « quoi » plus souvent que le « comment », et maintenant ils sont séparés. Nous pouvons importer le « quoi » à partir d'un fichier, tel que JSON, ou le charger à partir d'une base de données, rendant les mises à jour possibles sans modification de code, ou permettant aux non-développeurs de les faire.
  • Réutilisabilité : souvent, le « comment » est générique, et on peut le réutiliser, voire l'importer depuis une bibliothèque tierce.
  • Testabilité : chaque validation et la fonction du coureur de validation sont isolées, et nous pouvons les tester séparément.

Évitez les fichiers utilitaires monstres

De nombreux projets ont un fichier appelé utils.js, helpers.js ou misc.js dans lequel les développeurs ajoutent des fonctions utilitaires lorsqu'ils ne trouvent pas de meilleur emplacement pour elles. Souvent, ces fonctions ne sont jamais réutilisées ailleurs et restent pour toujours dans le fichier utilitaire, celui-ci continue donc de croître. C'est ainsi que naissent les fichiers utilitaires monstres.

Les fichiers de l'utilitaire Monster présentent plusieurs problèmes :

  • Mauvaise visibilité : puisque toutes les fonctions sont dans le même fichier, nous ne pouvons pas utiliser l'ouvre-fichier flou dans notre éditeur de code pour les trouver.
  • Ils peuvent survivre à leurs appelants : souvent, ces fonctions ne sont plus jamais réutilisées et restent dans la base de code, même après la suppression du code qui les utilisait.
  • Pas assez générique : de telles fonctions sont souvent conçues pour un seul cas d'utilisation et ne couvrent pas d'autres cas d'utilisation.

Voici mes règles empiriques :

  • Si la fonction est petite et utilisée une seule fois, conservez-la dans le même module où elle est utilisée.
  • Si la fonction est longue ou utilisée plus d'une fois, placez-la dans un fichier séparé dans le dossier util, shared ou helpers.
  • Si nous voulons plus d'organisation, au lieu de créer des fichiers comme utils/validators.js, nous pouvons regrouper les fonctions associées (chacune dans son propre fichier) dans un dossier : utils/validators/isEmail.js.

Évitez les exportations par défaut

Les modules JavaScript ont deux types d'exports. Le premier est les exportations nommées :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Qui peut être importé comme ceci :

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Et le deuxième est les exportations par défaut :

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Qui peut être importé comme ceci :

function FormattedAddress({ address, city, country, district, zip }) {
  return [address, zip, district, city, country]
    .filter(Boolean)
    .join(', ');
}

function getMapLink({ name, address, city, country, zip }) {
  return `https://www.google.com/maps/?q=${encodeURIComponent(
    [name, address, zip, city, country].filter(Boolean).join(', ')
  )}`;
}

function ShopsPage({ url, title, shops }) {
  return (
    <PageWithTitle url={url} title={title}>
      <Stack as="ul" gap="l">
        {shops.map(shop => (
          <Stack key={shop.name} as="li" gap="m">
            <Heading level={2}>
              <Link href={shop.url}>{shop.name}</Link>
            </Heading>
            {shop.address && (
              <Text variant="small">
                <Link href={getMapLink(shop)}>
                  <FormattedAddress {...shop} />
                </Link>
              </Text>
            )}
          </Stack>
        ))}
      </Stack>
    </PageWithTitle>
  );
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Je ne vois pas vraiment d’avantages aux exportations par défaut, mais elles présentent plusieurs problèmes :

  • Mauvais refactoring : renommer un module avec une exportation par défaut laisse souvent les importations existantes inchangées. Cela ne se produit pas avec les exportations nommées, où toutes les importations sont mises à jour après avoir renommé une fonction.
  • Incohérence : les modules exportés par défaut peuvent être importés sous n'importe quel nom, ce qui réduit la cohérence et la grépabilité de la base de code. Les exportations nommées peuvent également être importées sous un nom différent à l’aide du mot-clé as pour éviter les conflits de noms, mais c’est plus explicite et cela se fait rarement par accident.

Info : Nous parlons davantage de la grépabilité dans la section Écrire du code gréptable du chapitre Autres techniques.

Malheureusement, certaines API tierces, comme React.lazy() nécessitent des exportations par défaut, mais pour tous les autres cas, je m'en tiens aux exportations nommées.

Évitez les limes barillet

Un fichier baril est un module (généralement nommé index.js ou index.ts) qui réexporte un tas d'autres modules :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Le principal avantage réside dans des importations plus propres. Au lieu d'importer chaque module individuellement :

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Nous pouvons importer tous les composants à partir d'un fichier tonneau :

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cependant, les limes tonneau présentent plusieurs problèmes :

  • Coût de maintenance : nous devons ajouter une exportation de chaque nouveau composant dans un fichier baril, ainsi que des éléments supplémentaires tels que des types de fonctions utilitaires.
  • Coût de performance : la configuration du tremblement d'arbre est complexe, et les fichiers en baril entraînent souvent une augmentation de la taille du paquet ou des coûts d'exécution. Cela peut également ralentir le rechargement à chaud, les tests unitaires et les linters.
  • Importations circulaires : l'importation à partir d'un fichier Barrel peut provoquer une importation circulaire lorsque les deux modules sont importés à partir des mêmes fichiers Barrel (par exemple, le composant Button importe le composant Box).
  • Expérience du développeur : la navigation vers la définition de la fonction permet d'accéder au fichier Barrel au lieu du code source de la fonction ; et l'importation automatique peut être confuse quant à savoir s'il faut importer à partir d'un fichier baril au lieu d'un fichier source.

Info : TkDodo explique en détail les inconvénients des limes à barillet.

Les avantages des limes cylindriques sont trop mineurs pour justifier leur utilisation, je recommande donc de les éviter.

Un type de fichiers Barrel que je n'aime pas particulièrement est celui qui exporte un seul composant juste pour permettre de l'importer en tant que ./components/button au lieu de ./components/button/button.

Restez hydraté

Pour troller les DRYers (développeurs qui ne répètent jamais leur code), quelqu'un a inventé un autre terme : WET, écrivons tout deux fois, ou nous aimons taper, suggérant que nous devrions dupliquer le code à au moins deux fois jusqu'à ce que nous le remplaçons par une abstraction. C'est une blague, et je ne suis pas entièrement d'accord avec l'idée (parfois, il est acceptable de dupliquer du code plus de deux fois), mais c'est un bon rappel que toutes les bonnes choses sont meilleures avec modération.

Considérez cet exemple :

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Il s'agit d'un exemple extrême de séchage de code, qui ne rend pas le code plus lisible ou maintenable, surtout lorsque la plupart de ces constantes ne sont utilisées qu'une seule fois. Voir les noms de variables au lieu des chaînes réelles ici n'est d'aucune utilité.

Incorporons toutes ces variables supplémentaires. (Malheureusement, la refactorisation en ligne dans Visual Studio Code ne prend pas en charge les propriétés des objets en ligne, nous devons donc le faire manuellement.)

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Maintenant, nous avons beaucoup moins de code, et il est plus facile de comprendre ce qui se passe et de mettre à jour ou de supprimer des tests.

J’ai rencontré tellement d’abstractions désespérées dans les tests. Par exemple, ce modèle est très courant :

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Ce modèle tente d'éviter de répéter les appels mount(...) dans chaque scénario de test, mais il rend les tests plus déroutants qu'ils ne devraient l'être. Incorporons les appels mount() :

function FormattedAddress({ address, city, country, district, zip }) {
  return [address, zip, district, city, country]
    .filter(Boolean)
    .join(', ');
}

function getMapLink({ name, address, city, country, zip }) {
  return `https://www.google.com/maps/?q=${encodeURIComponent(
    [name, address, zip, city, country].filter(Boolean).join(', ')
  )}`;
}

function ShopsPage({ url, title, shops }) {
  return (
    <PageWithTitle url={url} title={title}>
      <Stack as="ul" gap="l">
        {shops.map(shop => (
          <Stack key={shop.name} as="li" gap="m">
            <Heading level={2}>
              <Link href={shop.url}>{shop.name}</Link>
            </Heading>
            {shop.address && (
              <Text variant="small">
                <Link href={getMapLink(shop)}>
                  <FormattedAddress {...shop} />
                </Link>
              </Text>
            )}
          </Stack>
        ))}
      </Stack>
    </PageWithTitle>
  );
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

De plus, le modèle beforeEach ne fonctionne que lorsque l'on souhaite initialiser chaque scénario de test avec les mêmes valeurs, ce qui est rarement le cas :

const file = 'pizza.jpg';
const prefix = file.slice(0, -4);
// → 'pizza'
Copier après la connexion
Copier après la connexion

Pour éviter certaines duplications lors du test des composants React, j'ajoute souvent un objet defaultProps et je le répartis dans chaque scénario de test :

const file = 'pizza.jpg';
const prefix = path.parse(file).name;
// → 'pizza'
Copier après la connexion
Copier après la connexion

De cette façon, nous n’avons pas trop de duplication, mais en même temps, chaque cas de test est isolé et lisible. La différence entre les cas de test est désormais plus claire car il est plus facile de voir les accessoires uniques de chaque cas de test.

Voici une variante plus extrême du même problème :

// my_feature_util.js
const noop = () => {};

export const Utility = {
  noop
  // Many more functions…
};

// MyComponent.js
function MyComponent({ onClick }) {
  return <button onClick={onClick}>Hola!</button>;
}

MyComponent.defaultProps = {
  onClick: Utility.noop
};
Copier après la connexion
Copier après la connexion

Nous pouvons intégrer la fonction beforeEach() de la même manière que nous l'avons fait dans l'exemple précédent :

const findByReference = (wrapper, reference) =>
  wrapper.find(reference);

const favoriteTaco = findByReference(
  ['Al pastor', 'Cochinita pibil', 'Barbacoa'],
  x => x === 'Cochinita pibil'
);

// → 'Cochinita pibil'
Copier après la connexion

J'irais encore plus loin et utiliserais la méthode test.each() car nous exécutons le même test avec un tas d'entrées différentes :

function MyComponent({ onClick }) {
  return <button onClick={onClick}>Hola!</button>;
}

MyComponent.defaultProps = {
  onClick: () => {}
};
Copier après la connexion

Maintenant, nous avons rassemblé toutes les entrées de test avec leurs résultats attendus en un seul endroit, ce qui facilite l'ajout de nouveaux cas de test.

Info : Consultez mes aide-mémoire Jest et Vitest.


Le plus grand défi avec les abstractions est de trouver un équilibre entre être trop rigide et trop flexible, et savoir quand commencer à abstraire les choses et quand s'arrêter. Cela vaut souvent la peine d’attendre pour voir si nous avons vraiment besoin d’abstraire quelque chose – bien souvent, il vaut mieux ne pas le faire.

C'est bien d'avoir un composant bouton global, mais s'il est trop flexible et comporte une douzaine d'accessoires booléens pour basculer entre différentes variantes, il sera difficile à utiliser. Cependant, si c'est trop rigide, les développeurs créeront leurs propres composants de bouton au lieu d'en utiliser un partagé.

Nous devons être vigilants quant à laisser les autres réutiliser notre code. Trop souvent, cela crée un couplage étroit entre des parties de la base de code qui devraient être indépendantes, ce qui ralentit le développement et entraîne des bugs.

Commencez à penser à :

  • Colocalisation du code associé dans le même fichier ou dossier pour faciliter sa modification, son déplacement ou sa suppression.
  • Avant d'ajouter une autre option à une abstraction, demandez-vous si ce nouveau cas d'utilisation y appartient vraiment.
  • Avant de fusionner plusieurs morceaux de code qui se ressemblent, demandez-vous s'ils résolvent réellement les mêmes problèmes ou s'ils se ressemblent simplement.
  • Avant de faire des tests SECs, demandez-vous si cela les rendrait plus lisibles et maintenables, ou si un peu de duplication de code ne serait pas un problème.

Si vous avez des commentaires, mastodontez-moi, tweetez-moi, ouvrez un ticket sur GitHub ou envoyez-moi un e-mail à artem@sapegin.ru. Obtenez votre exemplaire.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal