Accueil / Articles / Pourquoi les abstractions du frontend se transforment discrètement en dette technique

Pourquoi les abstractions du frontend se transforment discrètement en dette technique

Comprendre pourquoi les abstractions prématurées du frontend ajoutent une complexité cachée et comment déterminer quand des composants partagés, des hooks ou des utilitaires valent réellement la peine d’être créés.

2389 mots

Il y a un moment dans presque tous les projets frontend où ils cessent progressivement d’être des applications pour devenir des frameworks conçus pour soutenir des applications.

On intègre une bibliothèque de composants.

Puis arrive un système de conception.

Ensuite, une couche de gestion d’état est ajoutée.

Puis apparaît une abstraction pour la récupération de données.

Ensuite, un hook personnalisé entoure cette abstraction de récupération de données.

Puis quelqu’un écrit un composant de formulaire générique qui prend un objet de configuration décrivant le comportement des formulaires.

Bientôt, modifier quelque chose d’aussi simple qu’un bouton signifie parcourir six fichiers, trois niveaux d’abstraction, ainsi qu’une convention dont personne ne peut retracer l’inventeur.

Ce qui est étrange, c’est que aucune de ces choix individuels ne semblait irrationnelle au moment où ils ont été faits.

C’est là réellement le problème fondamental dans la manière dont les équipes frontend gèrent l’abstraction.

La plupart des abstractions ne sont pas intrinsèquement mauvaises, et beaucoup d’entre elles sont véritablement utiles. Le problème, c’est que les développeurs frontend sont devenus incroyablement doués pour créer des abstractions avant même d’avoir suffisamment de preuves que celles-ci sont réellement nécessaires.

Nous ne nous contentons plus de simplifier la complexité existante.

Nous abstrayons même la simple possibilité que de la complexité puisse apparaître un jour.

Cette habitude génère un type particulier de dette technique.

L’abstraction commence généralement par de bonnes intentions

Imaginez trois composants distincts, chacun chargé de récupérer les données des utilisateurs.

Le premier contient un peu de logique de chargement redondante.

Le deuxième répète presque le même schéma.

Le troisième fait quelque chose de légèrement différent à nouveau.

Quelqu’un remarque cette répétition et suggère :

« Nous devrions probablement intégrer cela dans une abstraction partagée. »

C’est une observation pertinente en soi.

Ainsi, l’équipe crée un hook personnalisé :

const { data, loading, error } = useUserData(userId);

Très bien organisé.

Puis un autre composant nécessite une petite modification de comportement.

Au lieu d’appeler directement l’API, l’équipe ajoute une autre option :

useUserData(userId, {
  includePermissions: true,
  cache: true,
  retry: 3,
});

Quelques mois plus tard, ce hook ne se contente plus de récupérer des enregistrements d’utilisateurs.

Il gère désormais le cache, les tentatives de réessai, les vérifications de permissions, les transformations des données, la normalisation des erreurs, les mises à jour optimistes, ainsi que diverses particularités liées aux fonctionnalités spécifiques.

La duplication initiale a disparu.

Mais quelque chose d’autre est apparu : un écart croissant entre le code et ce qu’il fait réellement.

Examiner un composant ne vous indique plus d’où proviennent ses données.

Il faut d’abord comprendre l’abstraction qui se trouve devant soi.

C’est le compromis dont personne ne parle.

L’abstraction n’élimine pas la complexité.

Elle la déplace simplement.

Parfois, ce déplacement est véritablement utile.

D’autres fois, on a simplement échangé cinq lignes de logique simple contre trois cents lignes de mécanismes internes.

L’abstraction a un coût

Dès le début, les développeurs apprennent à considérer la duplication comme un fardeau.

C’est justifié, car c’est souvent le cas.

Mais la duplication n’est loin d’être le seul type de complexité.

Les autres formes incluent :

  • l’indirection
  • la configuration
  • le comportement implicite
  • les API génériques
  • les dépendances cachées
  • les conventions
  • l’héritage
  • les composants d’enveloppe
  • le débogage qui n’existe que grâce à l’abstraction elle-même
  • la charge cognitive

Une certaine duplication est véritablement moins coûteuse qu’une abstraction complexe.

Comparez deux approches.

L’une répète un petit fragment de logique trois fois.

L’autre crée une utilité générique avec une douzaine de paramètres, justifiée par le fait que trois cas d’usage actuels coïncident à environ 60 pour cent.

La deuxième option semble plus soignée, plus « conçue ».

Cependant, elle peut aussi être considérablement plus difficile à maintenir.

C’est là que le développement frontend se heurte régulièrement à des problèmes.

Les équipes optimisent pour du **code DRY** plutôt que pour un **code facile à suivre**.

Ces objectifs ne sont pas interchangeables.

L’écosystème frontend encourage cela

Le développement frontend entretient une relation particulière avec l’abstraction, principalement parce que tout l’écosystème est conçu pour être structuré en couches.

Une seule application moderne peut facilement intégrer un framework de rendu pour la création de composants, une bibliothèque de routage, un gestionnaire d’état côté client, un outil distinct pour gérer l’état côté serveur, ainsi qu’une bibliothèque de formulaires accompagnée de son propre package de validation. En plus de cela, on trouve un système de conception, un ensemble de composants UI prédéfinis, une abstraction de stylisation superposée au CSS classique, un outil de compilation qui orchestre tout cela, et un framework de test qui surveille l’ensemble du système.

Chacun d’eux résout un problème concret en soi.

Les choses tournent mal dès que l’application commence à ajouter ses propres couches personnalisées par-dessus tout cela.

Au lieu d’utiliser directement le framework, le développeur recourt à un enveloppement interne autour de celui-ci.

Cet enveloppement dépend à son tour d’un autre enveloppement encore.

Bientôt, le modèle de programmation quotidien de l’équipe ne ressemble plus guère à la plateforme sous-jacente.

Cette tendance se manifeste particulièrement souvent dans les grandes organisations.

<AppPage>
  <DataBoundary>
    <PermissionGate>
      <FormContainer>
        <EntityEditor />
      </FormContainer>
    </PermissionGate>
  </DataBoundary>
</AppPage>

Chaque élément a un but défini.

Chaque couche a une raison documentée d’existence.

Mais dès qu’un problème survient, un développeur doit reconstituer mentalement toute la stack avant même d’arriver au code fonctionnel lui-même.

Cette reconstitution n’est pas gratuite, ni sur le plan cognitif ni en termes de temps consacré.

Les composants génériques sont souvent les plus problématiques

L’un des moyens les plus simples d’augmenter la complexité du frontend est de concevoir un composant censé anticiper toutes les exigences futures.

Cela commence de manière innocente :

<Button />

Puis cela évolue un peu :

<Button variant="primary" />

Puis cela continue de s’élargir :

<Button
  variant="primary"
  size="large"
  loading
  icon={...}
  permission="admin"
  analyticsEvent="save"
  confirm
/>

Finalement, on se retrouve avec quelque chose qui techniquement n’est plus un bouton.

C’est un framework miniature permettant d’afficher des actions arbitraires.

L’équipe se sent plus productive, car de nouveaux boutons peuvent désormais être créés via des configurations plutôt que par une implémentation à partir de zéro.

Mais la configuration reste du code, que cela paraisse ou non le cas.

Dans certains sens, c’est même pire, car la configuration cache le flux de contrôle réel.

Lorsque l’on lit vingt lignes de code simple, on peut comprendre exactement ce qui se passe.

Lorsque l’on lit vingt lignes de configuration, il peut falloir examiner l’implémentation du composant, suivre le parseur de configuration, déterminer les valeurs par défaut et comprendre quelles options interagissent discrètement entre elles.

Le code explicite a en fait été remplacé par un vocabulaire.

Ce vocabulaire peut être véritablement puissant.

Il peut tout aussi facilement devenir un dialecte que personne ne souhaite entretenir.

Le piège de la « résistance au futur »

La résistance au futur est généralement le meilleur argument avancé pour ajouter une abstraction.

« Nous pourrions en avoir besoin plus tard. »

« Il y aura probablement davantage de variantes par la suite. »

« Cela pourrait être réutilisé ailleurs dans l’application. »

« Construisons-le de manière générique dès le début. »

Occasionnellement, cet instinct est juste. Mais bien plus souvent, on ne dispose tout simplement pas encore d’informations suffisantes pour le savoir.

Le problème, c’est que toute abstraction repose sur des hypothèses concernant le problème. Abstraire trop tôt signifie fixer des choix architecturaux avant même de comprendre réellement ce que l’on construit. Et une fois que d’autres parties du codebase commencent à dépendre de cette abstraction, revenir en arrière devient très coûteux rapidement.

C’est précisément pour cette raison que l’abstraction précoce est plus risquée qu’il n’y paraît. Le code dupliqué est généralement facile à refactorer lorsque l’on est prêt. En revanche, une abstraction défectueuse a tendance à se propager.

Imaginez trois composants qui ressemblent. Vous pourriez laisser de côté la duplication pour l’instant. Si un véritable schéma partagé émerge avec le temps, vous pourrez alors l’extraire. Mais si vous passez directement à une abstraction généralisée, chaque cas d’usage futur sera contraint de s’adapter aux hypothèses que vous avez faites au départ.

À ce stade, l’abstraction cesse d’être une commodité pour devenir une contrainte. Ce qui devait éliminer la répétition finit par rendre les modifications plus difficiles que la duplication elle-même ne l’aurait jamais été.

De bonnes abstractions naissent généralement de la douleur

Les abstractions les plus solides ont tendance à ne pas être planifiées à l’avance. Elles sont découvertes au fil du temps.

Une équipe met en œuvre le même comportement à plusieurs reprises. Finalement, elle remarque quels éléments sont véritablement identiques et lesquels ne le semblent que superficiellement. Elle comprend alors ce qui diffère réellement. Ce n’est qu’alors qu’elle extrait le noyau stable et partagé.

Cela permet d’obtenir une base bien plus solide. On pourrait décrire cette séquence ainsi :

La duplication mène à la répétition, la répétition mène à la compréhension, et la compréhension mène à l’abstraction.

Cependant, les équipes frontend suivent souvent une voie différente :

La possibilité mène directement à l’abstraction, puis à la configuration, et enfin à la confusion.

La première approche nécessite plus de temps au départ. Mais sur toute la durée du projet, elle est généralement plus rapide dans l’ensemble, car l’abstraction qui en résulte reflète les connaissances réellement acquises par l’équipe grâce à son expérience.

Toute duplication ne doit pas être supprimée

C’est une vérité dérangeante pour les développeurs, car par défaut la duplication est perçue comme une erreur. Mais parfois, la duplication est le choix approprié.

Imaginons que deux composants partagent une logique de validation presque identique. Si cette logique ne compte que cinq lignes et que chaque composant est soumis à des règles métier différentes, conserver le code dupliqué peut être l’option la plus judicieuse.

Pourquoi ? Parce que les conserver séparés permet de préserver la compréhension locale. Quelqu’un qui travaille plus tard sur l’un des composants peut ajuster son comportement sans craindre de briser accidentellement l’autre.

Un code dupliqué signifie implicitement : ces deux éléments se ressemblent simplement pour l’instant.

Une abstraction affirme quelque chose de bien plus fort : ces deux éléments sont conçus pour être identiques et doivent évoluer ensemble à l’avenir.

C’est une affirmation plus forte. Il ne faut recourir à une abstraction que lorsque l’on croit véritablement en sa justesse.

Les abstractions doivent avoir une API restreinte

Un test pratique utile consiste à se demander : combien de temps faut-il réellement pour apprendre à utiliser correctement cette abstraction ?

Si cette liste continue de s’allonger, c’est probablement que vous observez une abstraction se transformer en framework.

const user = useUser(id);

const user = useUser(id, {
  cache: true,
  normalize: true,
  permissions: true,
  optimistic: false,
  retry: 3,
  suspense: false,
  transform: customTransform,
  mode: "editor",
});

Les équipes frontend ont besoin d’un budget d’abstraction

Au moment d’ajouter un nouveau concept, il est utile de se poser quelques questions.

Combien de fois ce schéma exact est-il déjà apparu ? Si la réponse est une seule fois, ne l’abstraitez pas pour l’instant. S’il s’est produit deux fois, considérez cela comme un motif de suspicion plutôt que comme une raison à agir. Cinq occurrences commencent à ressembler à des preuves réelles.

Ensuite : ces éléments changent-ils vraiment pour les mêmes raisons ? Le fait qu’ils ressemblent n’est pas une justification suffisante. Deux composants peuvent paraître presque identiques tout en évoluant selon des trajectoires complètement différentes pour des raisons commerciales distinctes — dans ce cas, les regrouper en une abstraction unique pourrait être une erreur.

Finalement : cette abstraction simplifie-t-elle réellement les cas du quotidien ? Pas des cas hypothétiques futurs, mais ceux auxquels les gens sont constamment confrontés. Si utiliser cette abstraction signifie devoir relire sa documentation à chaque fois, vous avez probablement remplacé un problème par un autre plus grave.

Le meilleur code frontend est souvent ennuyeux

Le développement logiciel possède une hiérarchie des statuts plutôt particulière. Une abstraction ingénieuse paraît plus impressionnante que trois composants simples. Un système générique semble avoir une architecture plus solide qu’une fonction basique. Un framework réutilisable paraît plus professionnel qu’un peu de code dupliqué.

Cependant, les systèmes en production ne récompensent pas un code qui semble sophistiqué. Ils récompensent plutôt celui que les développeurs peuvent modifier en toute sécurité.

Le code frontend le plus précieux est généralement de ce type ennuyeux. Lorsque vous ouvrez un composant, vous pouvez immédiatement voir d’où proviennent ses données. Vous savez exactement ce qui se déclenche lorsque l’utilisateur clique sur quelque chose. Vous pouvez modifier le markup directement. Vous pouvez suivre le flux des états. Vous pouvez localiser facilement l’appel API.

Vous ne devriez pas avoir à assimiler la philosophie de conception interne d’une équipe juste pour corriger une erreur. Ce n’est pas un signe d’ingénierie peu sophistiquée — c’est plutôt un signe d’une bonne ingénierie.

L’abstraction doit réduire la pensée, pas l’accroître

L’abstraction n’existe pas pour faire en sorte que le code paraisse réutilisable. Son but est de rendre un système plus facile à comprendre. C’est l’exigence que les équipes frontend doivent se fixer pour chaque abstraction.

Si une abstraction permet à dix composants de partager un comportement complexe sans forcer chaque développeur à comprendre comment ce comportement est mis en œuvre, alors elle remplit sa fonction. Si, au contraire, chaque développeur doit apprendre une API compliquée juste pour modifier un composant simple, l’abstraction travaille probablement contre vous.

Cette distinction est importante car le travail frontend est déjà intrinsèquement difficile. Les navigateurs sont complexes. Les interfaces utilisateur sont complexes. Garder l’état synchronisé est complexe. L’accessibilité est complexe. Les performances sont complexes. Il n’y a pas besoin d’ajouter une complexité supplémentaire à tout cela simplement pour que l’architecture paraisse plus sophistiquée sur papier.

La prochaine étape du développement frontend ne viendra probablement pas de la découverte d’un autre niveau d’abstraction. Elle proviendra plutôt d’une meilleure capacité à reconnaître quand il ne faut pas en créer un.

Les ingénieurs qui se distingueront ne seront pas nécessairement ceux qui peuvent concevoir le système de composants réutilisables le plus élaboré. Ce seront ceux qui savent analyser un problème et juger correctement s’il nécessite réellement un système.

Parfois, l’abstraction appropriée se résume à une seule fonction. Parfois, c’est un composant. Parfois, il s’agit simplement d’un module bien nommé. Et parfois, ce sont cinq lignes de code redondantes que n’importe qui dans l’équipe peut lire et comprendre immédiatement.

Distinguer ces différentes situations est la véritable compétence à développer.

Lectures complémentaires

  • Les modèles de conception React : du OOP classique aux Hooks modernes — Explique comment des modèles logiciels classiques tels que Singleton, Factory et Observer s’appliquent dans React, ainsi que des modèles spécifiques à React comme les HOC, les Hooks et les composants composés.
  • Comprendre les principes SOLID à l’aide d’exemples de code concrets — Ce guide détaille les cinq principes SOLID à l’aide d’exemples de code concrets, montrant comment ils s’appliquent dans des projets réels et des applications React.
  • Six techniques TypeScript qui transforment les types en véritable prévention des bogues — Découvrez comment les satisfactions, les unions étiquetées, les vérifications jamais effectuées, les valeurs inconnues, les types dérivés et les identifiants marqués permettent à TypeScript de détecter des bogues réels au moment de la compilation plutôt qu’en production.