Les design patterns JavaScript portent leurs fruits grâce à un code reconnaissable
Une équipe JavaScript peut connaître davantage de design patterns et malgré tout rendre sa base de code plus difficile à comprendre. La valeur métier vient d'un petit ensemble de conventions que les ingénieurs reconnaissent et appliquent de manière cohérente. Pour un vice-président de l'ingénierie ou un responsable technique, la vraie question est de savoir si ces conventions aident les évaluateurs et les nouveaux ingénieurs à comprendre une conception avant d'en examiner l'implémentation.
Un nom de pattern familier donne aux lecteurs une attente sur la structure et les responsabilités. Quand quelqu'un dit « cela appelle une Strategy », quatre mots peuvent remplacer dix minutes passées à expliquer une conception au tableau blanc. L'échange fonctionne lorsque tout le monde comprend quelles règles vont varier, comment les appelants vont les sélectionner et quelle interface ces règles partagent. Strategy désigne cette organisation : des règles interchangeables derrière une interface commune.
Le même raccourci s'applique lorsqu'un développeur propose « nous allons faire un Adapter de requête ». Un Adapter traduit une interface en une autre, ce qui permet à un évaluateur de commencer par vérifier la traduction. Des structures familières aident aussi un nouvel ingénieur à s'orienter dans la base de code. Ces deux avantages dépendent du fait que l'implémentation corresponde à l'attente créée par son nom.
Comprendre du code existant prend un temps considérable
Le travail que ces conventions cherchent à simplifier occupe déjà une part importante du développement logiciel. Une étude de terrain publiée dans IEEE Transactions on Software Engineering a suivi 78 développeurs professionnels sur sept projets industriels, en enregistrant 3 148 heures de travail. Environ 58 % relevaient de la catégorie « program comprehension » définie par les chercheurs : le temps consacré à comprendre le code. Les entreprises participantes utilisaient principalement Java et .NET, ce qui définit la population à l'origine de ce pourcentage.
Le 2026 State of Code de Sonar mesure une charge différente. Sonar indique que les utilisateurs moins fréquents de l'IA consacrent en moyenne 24 % de leur semaine de travail à des tâches pénibles, notamment le débogage de code mal documenté et la compréhension de systèmes existants. Sonar a un intérêt commercial à ce que les équipes donnent la priorité à la qualité du code. Les tâches pénibles et la compréhension du programme décrivent des activités différentes dans des populations différentes et doivent rester des mesures distinctes.
Pour une équipe qui choisit ses conventions, la question pratique est de savoir où ce travail de compréhension apparaît. Les développeurs peuvent avoir besoin de retracer un comportement hérité, d'isoler un état partagé entre des tests, d'intégrer davantage d'exigences dans une abstraction déjà sous tension, ou de maintenir un ancien comportement de décorateur pour un framework. Les exemples suivants montrent comment ces tâches apparaissent et quelles responsabilités une convention reconnaissable peut rendre plus faciles à trouver. Leur utilité dépend du choix d'une convention adaptée à l'exigence et de son application cohérente.
Un projet en tête ?
Planifiez un appel de 30 minutes avec nous.
Des experts senior pour vous aider à avancer plus vite : produit, tech, cloud & IA.
Les conventions JavaScript peuvent s'appuyer sur les mécanismes natifs du langage
Choisir une implémentation adaptée commence par le langage utilisé par l'équipe. Addison-Wesley a publié Design Patterns: Elements of Reusable Object-Oriented Software en octobre 1994, avec 23 patterns illustrés en C++ et Smalltalk. Ses auteurs, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides, sont devenus collectivement connus sous le nom de Gang of Four, ou GoF. JavaScript est apparu pour la première fois en décembre 1995, si bien que les exemples d'origine sont antérieurs à son modèle de langage.
Le catalogue GoF regroupe les conceptions selon les problèmes qu'elles traitent. Ces catégories aident un évaluateur à identifier la responsabilité en discussion avant d'examiner une implémentation. Une équipe JavaScript peut ensuite choisir les mécanismes du langage qui servent cette responsabilité.
| Catégorie | Responsabilité |
|---|---|
| Création | Contrôler la manière dont les objets sont créés. |
| Structure | Organiser les composants et limiter les dépendances inutiles. |
| Comportement | Organiser la communication entre les composants existants. |
JavaScript peut implémenter ces responsabilités avec des fonctions, des closures et des modules natifs. Une closure permet à une fonction de conserver l'accès aux liaisons environnantes après le retour de la fonction englobante ; un module natif expose certaines liaisons via le mécanisme standard d'import et d'export de JavaScript. Factory simple et Module sont aussi des idiomes JavaScript utiles. Le catalogue GoF contient à la place Factory Method et Abstract Factory, et ne comporte pas d'entrée Module.
L'héritage est une autre raison pour laquelle les implémentations JavaScript diffèrent des exemples d'origine. Lorsqu'une propriété est absente d'un objet, JavaScript suit la référence de prototype de cet objet et cherche le long de la chaîne. La recherche s'arrête lorsqu'elle trouve la propriété ou atteint un objet pour lequel Object.getPrototypeOf() renvoie null. La recherche délègue le long de la chaîne sans copier les propriétés des ancêtres, et MDN décrit principalement les classes comme une abstraction au-dessus de cet héritage fondé sur les prototypes.
La délégation crée un travail de lecture lorsque la définition propre d'un objet n'explique pas son comportement. Un ingénieur peut devoir inspecter des ancêtres successifs pour trouver ce comportement, et des chaînes plus longues rendent la recherche plus difficile. Certains ingénieurs JavaScript seniors apportent cette compétence depuis des projets antérieurs. L'équipe doit malgré tout rendre sa structure d'héritage compréhensible pour les ingénieurs qui l'apprennent.
La confidentialité dépend aussi du mécanisme choisi. JavaScript n'a pas de syntaxe pour un constructeur privé, même si un module, une closure ou une garde à l'exécution peuvent restreindre la construction. Les champs de classe #private ont une confidentialité appliquée par le moteur JavaScript, ce que l'ancienne syntaxe à base de fonctions constructrices ne peut pas exprimer directement. Les closures et WeakMap, une collection capable d'associer des données cachées à des objets, peuvent offrir une dissimulation comparable sans classes.
Les choix de création doivent rendre explicites la construction et la portée
Pour des ressources partagées comme des pools de connexions et des clients de feature flags, la première décision concerne la frontière à l'intérieur de laquelle le partage s'applique. Un Singleton fournit une instance commune à l'intérieur de cette frontière. Les modules ECMAScript (ES) fournissent souvent directement ce comportement : les imports se résolvent vers une identité de module, le module est évalué une seule fois dans son graphe d'imports, et les fonctions exportées utilisent son état de niveau supérieur.
« `javascript
// db.js
const pool = new Map();
export function getConnection(id)
export function addConnection(id, conn)
« `
Quatre fichiers importateurs qui se résolvent vers cette même instance de module utilisent le même Map. L'évaluation du module fournit le mécanisme d'instance partagée, donc une classe Singleton explicite n'ajoute ici aucun comportement nécessaire. Le nom familier getInstance() vient de Java et des exemples GoF. Il désigne une interface exposée aux appelants plutôt qu'une API JavaScript ou TypeScript intégrée.
La portée de l'instance doit malgré tout rester explicite. « L'unique » instance appartient à un graphe de modules donné, tandis qu'un worker, un autre processus ou une seconde copie du package sous node_modules peut recevoir une instance distincte. Compter les importateurs apprend donc peu de choses aux ingénieurs sur le nombre d'instances à l'échelle du déploiement. La coordination via l'état du module doit tenir compte de ces environnements d'exécution séparés.
Dans chaque environnement, l'état mutable partagé a aussi besoin d'un propriétaire. Les tests peuvent devenir dépendants de l'ordre à moins de réinitialiser ou d'isoler l'état, tandis que des données propres à une requête au niveau du module peuvent persister entre des requêtes rendues côté serveur. Réservez le partage au niveau du module aux ressources qui en ont réellement besoin, et injectez ces ressources dans la logique métier lorsque c'est possible. Le support natif des modules simplifie la construction tout en laissant en place ces responsabilités de gestion d'état.
Une Factory traite la construction elle-même : les appelants demandent un objet configuré via une fonction qui gère sa mise en place. La factory reçoit une configuration, la valide, dérive un état privé et renvoie des opérations publiques. Cette organisation est utile lorsqu'il faut créer plusieurs types d'entités ou lorsque les règles d'initialisation seraient autrement répétées sur plusieurs sites d'appel.
« `javascript
const RANKS = ;
export function createLogger( = )
const apiLog = createLogger();
apiLog.warn(« limite de débit à 80 % »);
« `
La validation établit quelle configuration la factory accepte. Object.hasOwn() limite les niveaux valides aux propriétés propres de l'objet, alors que level in RANKS accepterait aussi des clés héritées comme "toString" parce que in parcourt la chaîne de prototypes. Après validation, le seuil sélectionné contrôle quels messages atteignent console[rank](prefix + msg). La valeur 80 % est un contenu de message fictif servant à illustrer le logger configuré.
Les méthodes renvoyées conservent l'accès à threshold et write via une closure, tandis que les appelants n'ont d'accès direct à aucune de ces deux liaisons. Une seule factory peut produire des loggers configurés différemment avec les mêmes opérations publiques et des paramètres encapsulés distincts. Si les règles d'initialisation changent, l'équipe peut mettre à jour la factory au lieu de modifier chaque appelant. Les fonctions fournissent ici à la fois une confidentialité à l'exécution et une construction contrôlée.
Prototype introduit une ambiguïté de nommage qu'une revue ne peut pas trancher en disant seulement « prototype ». Le Prototype GoF crée des copies à partir d'un exemplaire configuré, tandis que les prototypes JavaScript fournissent une recherche de propriétés par délégation. L'exigence doit donc établir si les appelants ont besoin d'un comportement hérité ou de données indépendantes.
« `javascript
const baseVehicle = ;
const myCar = Object.create(baseVehicle);
myCar.make = « Toyota »;
console.log(myCar.wheels); // 4
const baseConfig = ;
const testConfig = structuredClone(baseConfig);
testConfig.headers.agent = « test »;
console.log(baseConfig.headers.agent); // « core »
« `
La première opération, Object.create(baseVehicle), relie le nouvel objet à baseVehicle, de sorte que myCar.wheels se résout par délégation. La valeur "Toyota" est une donnée d'exemple attachée à ce nouvel objet. La seconde opération, structuredClone(baseConfig), crée une copie profonde indépendante des données prises en charge. Modifier la valeur imbriquée de l'agent laisse donc la configuration d'origine inchangée.
La copie indépendante a un contrat précis. structuredClone() prend en charge les références circulaires dans les données compatibles, mais une fonction provoque DataCloneError ; les descripteurs de propriété, les getters et setters, les chaînes de prototypes personnalisées et les éléments privés de classe ne sont pas préservés. On ne peut donc pas s'attendre à ce qu'une instance de classe copiée conserve son comportement de prototype d'origine. La fonction fournit la partie « copie de données prises en charge » de Prototype, et les évaluateurs doivent vérifier que c'est bien ce comportement dont les appelants ont besoin.
Les frontières structurelles doivent contenir les changements de dépendances
Une fois la construction rendue explicite, la question suivante est de savoir comment les dépendances atteignent le reste de l'application. Remplacer un fournisseur SMS, un processeur de paiement ou un backend de recherche peut affecter un fichier ou trois cents, des ordres de grandeur illustratifs qui montrent pourquoi la frontière compte. Les conventions structurelles donnent au code des emplacements clairs pour l'implémentation privée, la traduction externe et le comportement ajouté. Ces emplacements déterminent jusqu'où se propage un changement de dépendance.
Pour une implémentation privée et une interface publique, les modules ES natifs sont le choix JavaScript normal. Avant les modules natifs, les fonctions immédiatement invoquées, des fonctions exécutées dès leur définition, et d'autres conventions créaient des frontières privées et publiques. Les modules ES expriment directement cette frontière en gardant non exportées les liaisons internes et en exposant les opérations dont les appelants ont besoin.
« `javascript
// inventory.js
const cache = new Map();
export function getSku(sku)
export async function refresh()
« `
Les consommateurs peuvent appeler getSku() et refresh(), tandis que le cache non exporté reste interne. Le rafraîchissement suit une séquence visible : demander "/api/inventory", lire la réponse JSON, puis stocker chaque élément sous item.sku. C'est la convention Module implémentée via la frontière propre au langage. Une construction distincte de type Module pattern nécessiterait une exigence supplémentaire pour la justifier.
Un Adapter étend cette définition de frontière à une interface contrôlée par quelqu'un d'autre. L'application définit sa propre opération, et une implémentation traduit cette opération vers l'API externe. Les kits de développement logiciel tiers, ou SDK, et les API héritées sont des raisons fréquentes de créer cette frontière. Elle est particulièrement utile lorsque le remplacement de la dépendance est une exigence réaliste.
« `javascript
class VendorSms
class SmsGateway
const gateway = new SmsGateway(new VendorSms());
« `
Les appelants utilisent SmsGateway.notify(recipient, message), et l'adapter traduit cette requête en VendorSms.send(to, body). Si le fournisseur déprécie send(), les ingénieurs ont un endroit clair où modifier la traduction. Les achats pourraient aussi trouver une meilleure offre pendant que le code applicatif conserve son vocabulaire existant. Les évaluateurs doivent vérifier que les hypothèses propres au fournisseur restent à l'intérieur de cette frontière.
Un Decorator crée une frontière différente en ajoutant un comportement autour d'une opération existante. Journalisation, autorisation, tentatives de reprise et télémétrie peuvent envelopper une fonction ou un objet tout en laissant son implémentation inchangée. Cela permet aux ingénieurs d'étendre des objets individuels sans développer un arbre d'héritage. Les composants d'ordre supérieur de React, qui enveloppent des composants pour ajouter un comportement, utilisent une approche similaire.
« `javascript
function withRetry(fn, attempts = 3)
const chargeCard = withRetry(rawChargeCard);
« `
L'enveloppe transmet à la fois les arguments et le this appelant via fn.apply(this, args). Sa boucle commence à un, continue jusqu'au nombre configuré de tentatives, renvoie le résultat attendu en cas de succès et relance l'erreur après le dernier échec. L'opération et la procédure de reprise restent séparées, ce qui rend les deux responsabilités visibles. Pour une opération de paiement comme rawChargeCard, la revue doit aussi établir qu'il est sûr de répéter un appel.
La combinaison d'enveloppes fait de l'ordre d'exécution une responsabilité supplémentaire. Par exemple, withLogging(withRetry(rawChargeCard)) place la journalisation autour de l'appel externe tandis que l'enveloppe de reprise contrôle les tentatives à l'intérieur. Le nom Decorator n'aide que lorsque l'équipe partage une compréhension de cet ordre. La syntaxe @ prise en charge par le compilateur ajoute une décision de compatibilité distincte, traitée après les conventions d'exécution.
Les conventions comportementales ont besoin de règles de cycle de vie et de sélection
Les frontières identifient où vit le comportement ; les conventions comportementales identifient comment les composants interagissent. Observer permet à un sujet changeant de notifier un ensemble arbitraire d'abonnés sans connaître leur identité ni leur nombre. Les flux pilotés par événements, les interfaces réactives et les flux en temps réel utilisent cette organisation, y compris les écouteurs du Document Object Model du navigateur, ou DOM, et l'EventEmitter de Node. La règle de notification est simple, mais chaque abonnement a aussi une durée de vie.
« `javascript
export function createStore(initial)
« `
Le store garde l'état et les écouteurs privés, renvoie l'état courant depuis getSnapshot(), et remplace l'état avant de notifier les abonnés. Les écritures doivent remplacer le snapshot plutôt que le muter sur place, afin que les lectures puissent réutiliser une référence stable jusqu'à ce qu'un changement survienne. Chaque abonnement renvoie une fonction de désabonnement qui supprime son callback de l'ensemble. Le sujet est propriétaire de la notification, tandis que l'abonné est propriétaire de l'appel à cette fonction de nettoyage.
Le nettoyage met fin à la durée de vie de l'abonnement et évite la fuite d'écouteurs obsolètes. Un callback non supprimé peut continuer à recevoir des notifications après le démontage de son composant, et il peut conserver sa closure ainsi que tout ce que cette closure capture. Un évaluateur qui reconnaît Observer doit donc suivre l'abonnement jusqu'au nettoyage. Le seul mécanisme de notification laisse cette responsabilité non résolue.
Le useSyncExternalStore de React fournit une intégration avec des règles explicites à la fois pour l'abonnement et pour les snapshots. Sa fonction subscribe doit renvoyer une fonction de désabonnement, et getSnapshot doit renvoyer une valeur mise en cache au lieu de créer un nouvel objet à chaque lecture. L'API prend en charge des snapshots cohérents de stores externes pendant le rendu concurrent, lorsque React peut préparer le travail de rendu par étapes qui se chevauchent ; le rendu serveur exige aussi getServerSnapshot. Pour les équipes React opérant à grande échelle, un abonnement sans nettoyage correspondant devrait bloquer la revue.
Strategy fait passer l'attention de la gestion des abonnés à la sélection entre règles métier interchangeables. Une instruction switch qui grossit peut devenir l'endroit où plusieurs algorithmes s'accumulent. Extraire ces algorithmes dans des fonctions ou des objets leur donne une convention d'appel commune. La sélection devient alors une opération explicite.
« `javascript
const shipping = ;
function quote(method, weightKg)
« `
Les formules sont des règles d'expédition illustratives, sans devise implicite ni tarifs commerciaux réels. quote() valide que la méthode demandée est une propriété propre, puis l'invoque avec le poids. Une simple vérification de vérité admettrait à tort une fonction héritée comme shipping["toString"]. La prise en compte des prototypes utilisée dans la factory de logger s'applique aussi à cette frontière de sélection.
Une fois la sélection maîtrisée, ajouter une quatrième méthode d'expédition exige une clé supplémentaire tout en laissant inchangées les stratégies existantes. Cela limite la part du comportement existant qu'un développeur doit modifier pour effectuer le changement. La convention donne à chaque règle remplaçable une signature commune. Les fonctions JavaScript fournissent directement ce contrat.
TypeScript rend certains contrats vérifiables par le compilateur
Les exemples d'exécution laissent plusieurs attentes à la charge des ingénieurs et des tests. TypeScript peut en vérifier certaines avant l'exécution : une interface décrit la forme qu'un adapter ou une strategy doit fournir, tandis que des définitions d'événements génériques utilisent des paramètres de type pour décrire les payloads Observer attendus. Les interfaces disparaissent toutefois du JavaScript émis, de sorte que leurs vérifications coexistent avec la validation à l'exécution. L'état partagé et la responsabilité des abonnements exigent toujours des décisions explicites.
La construction et la confidentialité montrent pourquoi cette distinction compte. Un constructeur privé TypeScript restreint les appels pendant la compilation, tandis que le JavaScript émis n'a pas d'équivalent pour imposer un constructeur privé. Les closures JavaScript et les champs #private assurent une dissimulation pendant l'exécution indépendamment du vérificateur de types. La restriction requise détermine donc le mécanisme à choisir.
| Sujet | Base JavaScript | Ajout TypeScript | Exemples pertinents |
|---|---|---|---|
| Encapsulation | Les closures et #private assurent une confidentialité à l'exécution. |
private, protected et readonly imposent des restrictions au vérificateur de types. |
Module et certaines implémentations de Singleton |
| Contrats | Des tests ou une validation explicite vérifient la structure attendue. | Les interfaces vérifient la forme pendant la compilation et sont ensuite effacées. | Adapter et Strategy |
| Initialisation | Les constructeurs affectent explicitement les champs. | Les propriétés de paramètre combinent déclaration de champ et initialisation. | Factory et Adapter |
| Forme du payload | Les valeurs publiées déterminent le payload à l'exécution. | Les définitions d'événements génériques vérifient les formes de payload prévues. | Observer |
Pour le module d'inventaire précédent, les types peuvent rendre explicites à la fois les données stockées et l'échec possible de la recherche. Un Sku contient un identifiant de type chaîne et un stock numérique, et le cache associe des chaînes à cette forme. Le type de retour indique aux appelants qu'un élément demandé peut être absent.
« `typescript
// inventory.ts
export type Sku = ;
const cache = new Map
export function getSku(sku: string): Sku | undefined
// consumer.ts
import type from « ./inventory »;
import from « ./inventory »;
« `
Les deux imports servent des objectifs différents. La déclaration import type est effacée, de sorte que cet import partagé uniquement de type ajoute zéro octet au bundle de production. L'import de valeur fournit getSku pour l'usage à l'exécution. Les types et les exports exécutables peuvent partager une frontière de module tout en ayant des effets différents sur le code généré.
Pour la frontière SMS, une interface rend explicite le contrat de notification exigé par l'application. SmsGateway implements Notifier oblige le compilateur à vérifier ce contrat. Un adapter de remplacement avec une forme incorrecte échoue à la vérification avant l'exécution.
« `typescript
declare class VendorSms
interface Notifier
class SmsGateway implements Notifier {
constructor(private client: VendorSms)
async notify(recipient: { phone: string }, message: string) {
await this.client.send(recipient.phone, message);
}
}
« `
Le contrat déclaré exige Promise, une promesse qui s'achève sans renvoyer de valeur. Le notify() asynchrone préserve ce contrat en attendant l'opération du fournisseur. Pendant ce temps, constructor(private client: VendorSms) est une propriété de paramètre : elle déclare et initialise le champ dans la signature du constructeur. Ces vérifications et déclarations abrégées laissent toutefois la traduction elle-même à l'examen des ingénieurs.
Strategy reçoit une vérification similaire du compilateur via ShippingRate. L'interface inclut un libellé d'affichage et une opération de tarification, ce qui permet aux appelants d'accepter un tarif via un contrat commun. Omettre price() devient un échec à la compilation plutôt qu'une erreur découverte au moment du paiement.
« `typescript
interface ShippingRate
const express: ShippingRate = ;
function quote(rate: ShippingRate, weightKg: number)
« `
L'adoption du langage apporte un contexte pour l'usage de ces contrats, même si les mesures d'adoption répondent à des questions différentes. L'Octoverse 2025 de GitHub a compté 2 636 006 contributeurs mensuels TypeScript en août 2025, le plaçant en première position selon cette mesure d'activité et environ 42 000 contributeurs devant Python. GitHub a un intérêt commercial dans l'activité des développeurs via sa plateforme de développement. L'enquête développeurs 2025 de Stack Overflow a placé JavaScript en tête pour l'usage déclaré des langages, à 66 % des répondants ayant répondu à cette question ; l'activité des contributeurs et l'usage autodéclaré peuvent produire des classements différents parce qu'ils mesurent des choses différentes.
Les performances du compilateur sont une autre considération pour les équipes qui utilisent TypeScript. Microsoft a rendu TypeScript 7.0 généralement disponible le 8 juillet 2026, en utilisant un portage natif en Go du compilateur, et a indiqué une amélioration de vitesse de 10x avec des résultats variant selon la charge de travail. Microsoft a un intérêt commercial dans l'adoption de son écosystème de développement. Une compilation plus rapide peut améliorer les performances de build tout en laissant inchangées les responsabilités d'exécution, y compris l'état partagé des modules.
La modernisation des Decorator doit suivre la compatibilité du framework
Les changements du compilateur affectent aussi la syntaxe de décorateur qu'un projet peut utiliser. L'enveloppe de reprise précédente implémente Decorator via une fonction ordinaire, tandis que les transformations @ de TypeScript et d'ECMAScript opèrent sur des classes et des éléments de classe. Leur terminologie commune ne fait pas automatiquement d'une transformation @ une implémentation du pattern GoF. L'exigence de conception et les règles de transformation du compilateur nécessitent donc des décisions distinctes.
La décision du compilateur compte parce que TypeScript prend en charge des modèles de décorateurs incompatibles. TypeScript 5.0, publié en mars 2023, a introduit la prise en charge des décorateurs ECMAScript plus récents sans exiger experimentalDecorators. Activer ce flag sélectionne l'ancienne sémantique, c'est-à-dire les règles qui déterminent comment les décorateurs sont vérifiés, émis et appelés. Ce choix modifie à la fois la convention d'appel et les capacités disponibles.
| Point de compatibilité | Décorateurs hérités | Décorateurs récents |
|---|---|---|
| Arguments du décorateur de méthode | Cible, clé de propriété et descripteur | Méthode et objet de contexte |
| Décorateurs de paramètre | Pris en charge | Non pris en charge |
emitDecoratorMetadata |
Compatible avec le modèle hérité | Incompatible |
| Sélection du compilateur | Sélectionnés par experimentalDecorators |
Pris en charge sans ce flag |
Un décorateur de mesure du temps montre la convention d'appel la plus récente. Il reçoit la méthode d'origine et un ClassMethodDecoratorContext, obtient un libellé à partir de String(context.name), et renvoie une enveloppe asynchrone. L'appel attendu se trouve à l'intérieur de try, de sorte que finally termine la mesure une fois l'opération stabilisée.
« `typescript
function timed(
originalMethod: any,
context: ClassMethodDecoratorContext
)
class Reports
« `
Les frameworks existants peuvent au contraire exiger l'ancien modèle. De nombreux projets Angular, NestJS et TypeORM dépendent des décorateurs hérités, et certains utilisent des décorateurs de paramètre ou des métadonnées de constructeur émises, c'est-à-dire des informations sur les types du constructeur rendues disponibles à l'exécution. Leurs mécanismes diffèrent, donc la compatibilité doit être vérifiée par rapport aux versions exactes des frameworks utilisés. Changer de sémantique peut casser ces dépendances même lorsque la notation visible @ semble familière.
Le paramètre du compilateur s'applique aussi à l'ensemble d'une compilation : la sémantique héritée ne peut pas servir certains décorateurs pendant que la sémantique récente en sert d'autres. Microsoft a indiqué que experimentalDecorators resterait disponible dans un avenir prévisible. Pour les projets qui exigent l'ancien comportement, conserver ce flag peut donc être une décision de maintenance défendable.
Une migration doit commencer par identifier les versions des frameworks, valider leur prise en charge et sélectionner une sémantique compatible avant de planifier les changements. Traiter ce travail comme un simple nettoyage de flag de compilateur inverse cet ordre. Les équipes qui maintiennent des applications dépendantes de métadonnées ont besoin d'un plan de mise à niveau qui respecte ces dépendances. Le modèle le plus récent devient utilisable lorsque les exigences du framework du projet le permettent.
Une abstraction doit mériter le travail qu'elle crée
Les décisions de compatibilité montrent comment une convention peut créer des obligations au-delà de son code visible. Les fonctionnalités du langage peuvent aussi éliminer le besoin d'une implémentation explicite de pattern. En 1996, Peter Norvig a constaté que 16 des 23 patterns GoF devenaient invisibles ou plus simples en Lisp parce que le langage fournissait leur mécanique. Les fonctions de première classe, c'est-à-dire des fonctions qui peuvent être transmises et stockées comme des valeurs, et les modules étaient pertinents dans cette comparaison, et JavaScript possède les deux.
Paul Graham a formulé un argument plus tranché dans Revenge of the Nerds : « Quand je vois des patterns dans mes programmes, j'y vois un signe de problème. » La préoccupation de Graham était qu'une structure répétée puisse révéler une abstraction inadéquate, y compris dans les cas où les programmeurs répètent manuellement ce qu'une macro pourrait exprimer par transformation de code. Un nom hypothétique comme AbstractHandlerFactoryProvider illustre le risque d'accumuler les couches. Chaque couche doit apporter au lecteur une information utile.
Une couche initialement utile peut aussi se dégrader à mesure que les exigences évoluent. Sandi Metz décrit ce processus dans The Wrong Abstraction : un ingénieur crée du code commun, puis une exigence presque compatible pousse un autre ingénieur à ajouter des paramètres. D'autres exigences apportent encore plus d'accommodements, tandis que la réticence à modifier la conception établie laisse s'accumuler les branches conditionnelles. Le fichier devient plus difficile à modifier, ce qui conduit Metz à conclure que la duplication peut coûter moins cher qu'une abstraction inadaptée.
Le mécanisme est un changement dans la manière dont les ingénieurs évaluent les nouvelles exigences. Une fois que préserver l'abstraction partagée devient la priorité, chaque exigence est jugée selon la manière dont elle peut s'insérer dans cette abstraction. Les paramètres et les branches deviennent la réponse admise, même lorsqu'ils obscurcissent le comportement d'origine. Toute personne qui modifie ensuite le fichier doit comprendre les exceptions accumulées.
Dan Abramov décrit une expérience différente dans l'écosystème React dans Goodbye, Clean Code. Il a remplacé l'implémentation répétitive d'un collègue par une abstraction plus élégante, puis l'a vue devenir le fichier le plus difficile à modifier du projet. Son conseil était : « Laissez le clean code vous guider. Puis laissez-le partir. » Ce témoignage montre comment une abstraction choisie pour son élégance peut compliquer les changements ultérieurs.
Une revue peut traiter ce risque en demandant l'exigence qui se cache derrière chaque point d'extension. Un besoin démontré donne aux évaluateurs quelque chose de concret à partir de quoi juger l'interface ajoutée. Un hook pour une exigence future possible crée immédiatement du travail de lecture, donc son usage proposé doit justifier ce coût. L'abstraction doit servir une exigence que l'équipe est capable de nommer.
Ce test laisse de la place à des approches en dehors du catalogue GoF. Ses patterns restent un vocabulaire professionnel utile, tandis que des systèmes sérieux peuvent aussi être construits dans un style fonctionnel sans dépendre de ce catalogue. De nombreux ingénieurs seniors consulteraient d'abord Patterns of Enterprise Application Architecture face à une question de conception pratique. Pour des équipes JavaScript qui utilisent des fonctions et des conceptions orientées prototype, un défaut fondé sur les classes et priorisant la réutilisation peut introduire une mécanique dont l'exigence n'a pas besoin.
Standardisez quatre ou cinq conventions utiles, puis faites-les respecter
Choisir des abstractions adaptées devient plus simple lorsque le dépôt dispose d'un petit ensemble explicite de conventions. Deux patterns partiellement implémentés peuvent créer plus de travail de découverte qu'un seul pattern adéquat utilisé de manière cohérente, parce que les lecteurs doivent d'abord déterminer quelle convention s'applique. Un nom partagé compresse la communication lorsque le code donne à ce nom une signification stable.
Commencez par identifier les quatre ou cinq patterns que la base de code utilise réellement, puis documentez-les dans un guide d'une page. Donnez à chacun un exemple et un objectif, présentez le guide dès le premier jour d'un ingénieur, et référez-vous-y pendant les revues. Faites respecter les choix documentés et excluez les patterns qui n'ont pas d'usage local. La documentation et l'application en revue sont souvent négligées, alors qu'elles transforment des préférences individuelles en convention sur laquelle l'équipe peut compter.
Le guide doit consigner les décisions d'implémentation que les évaluateurs peuvent vérifier : où l'état partagé est autorisé, comment les adapters traduisent les dépendances, qui nettoie les abonnements, et quelle sémantique de décorateur les frameworks exigent. Les fonctions simples, les closures, les modules ES natifs et les API de framework existantes sont des implémentations valides lorsqu'elles répondent à l'exigence documentée. Pour chaque abstraction proposée, exigez un cas d'usage nommé et un exemple du changement qu'elle doit prendre en charge. Les évaluateurs devraient approuver une implémentation plus simple lorsqu'elle satisfait ces exigences.
Réflexions finales
Pour les responsables de l'ingénierie, la valeur des design patterns JavaScript réside dans la fiabilité avec laquelle les équipes peuvent comprendre et modifier le code. Un nom familier n'aide que lorsque son implémentation répond à une attente partagée. Ajouter davantage de patterns à la base de code n'établit pas cette cohérence.
Traitez les conventions comme des décisions opérationnelles. Les ingénieurs doivent savoir où l'état partagé a sa place, comment les dépendances externes sont contenues et à qui appartient le nettoyage des abonnements. TypeScript peut vérifier certains contrats avant l'exécution, mais il ne peut pas trancher ces questions de responsabilité. La compatibilité des frameworks doit aussi orienter les plans de modernisation, en particulier lorsque les applications dépendent de décorateurs hérités.
Soutenez un petit ensemble documenté de conventions via l'onboarding et la revue de code. Demandez aux équipes de justifier les nouvelles abstractions par rapport à des exigences qu'elles peuvent nommer, plutôt qu'à de possibles usages futurs. Les modules natifs, les fonctions et les closures peuvent répondre à ces exigences avec moins de mécanique.
Jugez le résultat à l'aune du travail que les ingénieurs doivent fournir pour effectuer un changement. Un évaluateur peut-il localiser la responsabilité pertinente ? Un nouvel ingénieur peut-il comprendre la frontière ? Un changement de dépendance peut-il rester contenu ? Ces questions donnent aux dirigeants une base plus utile pour évaluer les choix de conception que le nombre de patterns adoptés par une équipe.
Un projet en tête ?
Planifiez un appel de 30 minutes avec nous.
Des experts senior pour vous aider à avancer plus vite : produit, tech, cloud & IA.


