Le guide complet des modèles de conception JavaScript
Publié: 2022-03-11En tant que bon développeur JavaScript, vous vous efforcez d'écrire du code propre, sain et maintenable. Vous résolvez des défis intéressants qui, bien qu'uniques, ne nécessitent pas nécessairement des solutions uniques. Vous vous êtes probablement retrouvé à écrire du code qui ressemble à la solution d'un problème entièrement différent que vous avez traité auparavant. Vous ne le savez peut-être pas, mais vous avez utilisé un modèle de conception JavaScript . Les modèles de conception sont des solutions réutilisables aux problèmes courants de conception de logiciels.
Au cours de la durée de vie d'un langage, de nombreuses solutions réutilisables de ce type sont créées et testées par un grand nombre de développeurs de la communauté de ce langage. C'est grâce à cette expérience combinée de nombreux développeurs que de telles solutions sont si utiles car elles nous aident à écrire du code de manière optimisée tout en résolvant le problème à résoudre.
Les principaux avantages que nous obtenons des modèles de conception sont les suivants :
- Ce sont des solutions qui ont fait leurs preuves : étant donné que les modèles de conception sont souvent utilisés par de nombreux développeurs, vous pouvez être certain qu'ils fonctionnent. Et non seulement cela, vous pouvez être certain qu'ils ont été révisés plusieurs fois et que des optimisations ont probablement été mises en œuvre.
- Ils sont facilement réutilisables : les modèles de conception documentent une solution réutilisable qui peut être modifiée pour résoudre plusieurs problèmes particuliers, car ils ne sont pas liés à un problème spécifique.
- Ils sont expressifs : les modèles de conception peuvent expliquer une grande solution de manière assez élégante.
- Ils facilitent la communication : lorsque les développeurs sont familiarisés avec les modèles de conception, ils peuvent plus facilement communiquer entre eux sur les solutions potentielles à un problème donné.
- Ils évitent le besoin de refactoriser le code : si une application est écrite avec des modèles de conception à l'esprit, il arrive souvent que vous n'ayez pas besoin de refactoriser le code plus tard, car appliquer le modèle de conception correct à un problème donné est déjà une solution optimale. Solution.
- Ils réduisent la taille de la base de code : étant donné que les modèles de conception sont généralement des solutions élégantes et optimales, ils nécessitent généralement moins de code que les autres solutions.
Je sais que vous êtes prêt à vous lancer à ce stade, mais avant de tout savoir sur les modèles de conception, passons en revue quelques bases de JavaScript.
Une brève histoire de JavaScript
JavaScript est aujourd'hui l'un des langages de programmation les plus populaires pour le développement Web. Il a été initialement conçu comme une sorte de "colle" pour divers éléments HTML affichés, connus sous le nom de langage de script côté client, pour l'un des premiers navigateurs Web. Appelé Netscape Navigator, il ne pouvait afficher que du HTML statique à l'époque. Comme vous pouvez le supposer, l'idée d'un tel langage de script a conduit à des guerres de navigateurs entre les grands acteurs de l'industrie du développement de navigateurs à l'époque, tels que Netscape Communications (aujourd'hui Mozilla), Microsoft et d'autres.
Chacun des grands acteurs voulait pousser sa propre implémentation de ce langage de script, donc Netscape a créé JavaScript (en fait, Brendan Eich l'a fait), Microsoft a créé JScript, et ainsi de suite. Comme vous pouvez l'imaginer, les différences entre ces implémentations étaient importantes, de sorte que le développement pour les navigateurs Web a été fait par navigateur, avec les autocollants les mieux consultés fournis avec une page Web. Il est vite devenu évident que nous avions besoin d'un standard, une solution multi-navigateurs qui unifierait le processus de développement et simplifierait la création de pages Web. Ce qu'ils ont trouvé s'appelle ECMAScript.
ECMAScript est une spécification de langage de script standardisée que tous les navigateurs modernes essaient de prendre en charge, et il existe plusieurs implémentations (vous pourriez dire des dialectes) d'ECMAScript. Le plus populaire est le sujet de cet article, JavaScript. Depuis sa sortie initiale, ECMAScript a standardisé beaucoup de choses importantes, et pour ceux qui s'intéressent davantage aux détails, il existe une liste détaillée d'éléments standardisés pour chaque version d'ECMAScript disponible sur Wikipedia. La prise en charge du navigateur pour les versions ECMAScript 6 (ES6) et supérieures est encore incomplète et doit être transpilée vers ES5 afin d'être entièrement prise en charge.
Qu'est-ce que JavaScript ?
Afin de saisir pleinement le contenu de cet article, faisons une introduction à certaines caractéristiques de langage très importantes dont nous devons être conscients avant de nous plonger dans les modèles de conception JavaScript. Si quelqu'un vous demandait "Qu'est-ce que JavaScript?" vous pourriez répondre quelque part dans les lignes de:
JavaScript est un langage de programmation léger, interprété et orienté objet avec des fonctions de première classe, plus communément appelées langage de script pour les pages Web.
La définition susmentionnée signifie que le code JavaScript a une faible empreinte mémoire, est facile à mettre en œuvre et facile à apprendre, avec une syntaxe similaire aux langages populaires tels que C++ et Java. C'est un langage de script, ce qui signifie que son code est interprété au lieu d'être compilé. Il prend en charge les styles de programmation procéduraux, orientés objet et fonctionnels, ce qui le rend très flexible pour les développeurs.
Jusqu'à présent, nous avons examiné toutes les caractéristiques qui ressemblent à de nombreux autres langages, alors examinons ce qui est spécifique à JavaScript par rapport aux autres langages. Je vais énumérer quelques caractéristiques et faire de mon mieux pour expliquer pourquoi elles méritent une attention particulière.
JavaScript prend en charge les fonctions de première classe
Cette caractéristique était difficile à saisir pour moi lorsque je débutais avec JavaScript, car je venais d'un milieu C/C++. JavaScript traite les fonctions comme des citoyens de première classe, ce qui signifie que vous pouvez passer des fonctions en tant que paramètres à d'autres fonctions comme vous le feriez pour n'importe quelle autre variable.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })JavaScript est basé sur un prototype
Comme c'est le cas avec de nombreux autres langages orientés objet, JavaScript prend en charge les objets, et l'un des premiers termes qui vient à l'esprit lorsque l'on pense aux objets est les classes et l'héritage. C'est là que cela devient un peu délicat, car le langage ne prend pas en charge les classes dans sa forme en langage clair, mais utilise plutôt quelque chose appelé héritage basé sur un prototype ou basé sur une instance.
Ce n'est que maintenant, dans ES6, que la classe de termes formels est introduite, ce qui signifie que les navigateurs ne le prennent toujours pas en charge (si vous vous souvenez, au moment de la rédaction, la dernière version ECMAScript entièrement prise en charge est la 5.1). Il est important de noter, cependant, que même si le terme "classe" est introduit dans JavaScript, il utilise toujours l'héritage basé sur un prototype sous le capot.
La programmation basée sur des prototypes est un style de programmation orientée objet dans lequel la réutilisation du comportement (connue sous le nom d'héritage) est effectuée via un processus de réutilisation d'objets existants via des délégations qui servent de prototypes. Nous approfondirons cela une fois que nous aurons atteint la section des modèles de conception de l'article, car cette caractéristique est utilisée dans de nombreux modèles de conception JavaScript.
Boucles d'événements JavaScript
Si vous avez de l'expérience avec JavaScript, vous connaissez sûrement le terme fonction de rappel . Pour ceux qui ne connaissent pas le terme, une fonction de rappel est une fonction envoyée en tant que paramètre (rappelez-vous, JavaScript traite les fonctions comme des citoyens de première classe) à une autre fonction et est exécutée après le déclenchement d'un événement. Ceci est généralement utilisé pour s'abonner à des événements tels qu'un clic de souris ou une pression sur un bouton du clavier.
Chaque fois qu'un événement, auquel est attaché un écouteur, se déclenche (sinon l'événement est perdu), un message est envoyé à une file d'attente de messages qui sont traités de manière synchrone, de manière FIFO (premier entré, premier sorti ). C'est ce qu'on appelle la boucle d'événements .
Chacun des messages de la file d'attente est associé à une fonction. Une fois qu'un message est retiré de la file d'attente, le moteur d'exécution exécute complètement la fonction avant de traiter tout autre message. C'est-à-dire que si une fonction contient d'autres appels de fonction, ils sont tous exécutés avant de traiter un nouveau message de la file d'attente. C'est ce qu'on appelle l'exécution jusqu'à l'achèvement.
while (queue.waitForMessage()) { queue.processNextMessage(); } La queue.waitForMessage() attend de manière synchrone les nouveaux messages. Chacun des messages en cours de traitement possède sa propre pile et est traité jusqu'à ce que la pile soit vide. Une fois terminé, un nouveau message est traité à partir de la file d'attente, s'il y en a une.
Vous avez peut-être également entendu dire que JavaScript est non bloquant, ce qui signifie que lorsqu'une opération asynchrone est effectuée, le programme est capable de traiter d'autres choses, telles que la réception d'une entrée utilisateur, en attendant que l'opération asynchrone se termine, sans bloquer le principal fil d'exécution. C'est une propriété très utile de JavaScript et un article entier pourrait être écrit uniquement sur ce sujet ; cependant, cela sort du cadre de cet article.
Que sont les modèles de conception ?
Comme je l'ai déjà dit, les modèles de conception sont des solutions réutilisables aux problèmes courants de conception de logiciels. Jetons un coup d'œil à certaines des catégories de modèles de conception.
Proto-modèles
Comment crée-t-on un motif ? Supposons que vous reconnaissiez un problème courant et que vous disposiez de votre propre solution unique à ce problème, qui n'est ni reconnue ni documentée à l'échelle mondiale. Vous utilisez cette solution à chaque fois que vous rencontrez ce problème, et vous pensez qu'elle est réutilisable et que la communauté des développeurs pourrait en bénéficier.
Est-ce que cela devient immédiatement un modèle ? Heureusement, non. Souvent, on peut avoir de bonnes pratiques d'écriture de code et confondre simplement quelque chose qui ressemble à un modèle avec un modèle alors qu'en fait, ce n'est pas un modèle.
Comment pouvez-vous savoir quand ce que vous pensez reconnaître est en fait un modèle de conception ?
En obtenant l'avis d'autres développeurs à ce sujet, en connaissant le processus de création d'un modèle lui-même et en vous familiarisant avec les modèles existants. Il y a une phase qu'un pattern doit traverser avant de devenir un pattern à part entière, et c'est ce qu'on appelle un proto-pattern.
Un proto-pattern est un futur pattern s'il passe une certaine période de test par divers développeurs et scénarios où le pattern s'avère utile et donne des résultats corrects. Il y a une quantité assez importante de travail et de documentation - dont la plupart sort du cadre de cet article - à faire afin de faire reconnaître un modèle à part entière par la communauté.
Anti-modèles
Comme un design pattern représente une bonne pratique, un anti-pattern représente une mauvaise pratique.
Un exemple d'anti-pattern serait de modifier le prototype de la classe Object . Presque tous les objets de JavaScript héritent de Object (rappelez-vous que JavaScript utilise l'héritage basé sur un prototype), alors imaginez un scénario dans lequel vous avez modifié ce prototype. Les modifications apportées au prototype d' Object seraient visibles dans tous les objets qui héritent de ce prototype , c'est-à-dire la plupart des objets JavaScript . C'est une catastrophe imminente.
Un autre exemple, similaire à celui mentionné ci-dessus, consiste à modifier des objets qui ne vous appartiennent pas. Un exemple de ceci serait de remplacer une fonction à partir d'un objet utilisé dans de nombreux scénarios dans l'ensemble de l'application. Si vous travaillez avec une grande équipe, imaginez la confusion que cela entraînerait ; vous rencontreriez rapidement des collisions de noms, des implémentations incompatibles et des cauchemars de maintenance.
Semblable à l'utilité de connaître toutes les bonnes pratiques et solutions, il est également très important de connaître les mauvaises. De cette façon, vous pouvez les reconnaître et éviter de faire l'erreur dès le départ.
Catégorisation des modèles de conception
Les modèles de conception peuvent être classés de plusieurs manières, mais la plus populaire est la suivante :
- Modèles de conception de création
- Modèles de conception structurelle
- Modèles de conception comportementaux
- Modèles de conception de concurrence
- Modèles de conception architecturale
Modèles de conception de création
Ces patrons traitent des mécanismes de création d'objets qui optimisent la création d'objets par rapport à une approche basique. La forme de base de la création d'objets peut entraîner des problèmes de conception ou une complexité accrue de la conception. Les modèles de conception créationnels résolvent ce problème en contrôlant d'une manière ou d'une autre la création d'objets. Certains des modèles de conception populaires dans cette catégorie sont :
- Méthode d'usine
- Usine abstraite
- Constructeur
- Prototype
- Singleton
Modèles de conception structurelle
Ces modèles traitent des relations d'objet. Ils garantissent que si une partie d'un système change, l'ensemble du système n'a pas besoin de changer en même temps. Les modèles les plus populaires dans cette catégorie sont :
- Adaptateur
- Pont
- Composite
- Décorateur
- Façade
- Poids mouche
- Procuration
Modèles de conception comportementaux
Ces types de modèles reconnaissent, implémentent et améliorent la communication entre des objets disparates dans un système. Ils permettent de s'assurer que les parties disparates d'un système disposent d'informations synchronisées. Des exemples populaires de ces modèles sont :
- Chaîne de responsabilité
- Commander
- Itérateur
- Médiateur
- Mémento
- Observateur
- État
- Stratégie
- Visiteur
Modèles de conception de concurrence
Ces types de modèles de conception traitent des paradigmes de programmation multithread. Certains des plus populaires sont :
- Objet actif
- Réaction nucléaire
- Planificateur
Modèles de conception architecturale
Modèles de conception utilisés à des fins architecturales. Certains des plus célèbres sont :
- MVC (Modèle-Vue-Contrôleur)
- MVP (Modèle-Vue-Présentateur)
- MVVM (Modèle-Vue-VueModèle)
Dans la section suivante, nous allons examiner de plus près certains des modèles de conception susmentionnés avec des exemples fournis pour une meilleure compréhension.
Exemples de modèles de conception
Chacun des modèles de conception représente un type spécifique de solution à un type spécifique de problème. Il n'y a pas d'ensemble universel de modèles qui soit toujours le meilleur ajustement. Nous devons savoir quand un modèle particulier s'avérera utile et s'il fournira une valeur réelle. Une fois que nous nous sommes familiarisés avec les modèles et les scénarios auxquels ils conviennent le mieux, nous pouvons facilement déterminer si un modèle spécifique convient ou non à un problème donné.
N'oubliez pas que l'application du mauvais modèle à un problème donné peut entraîner des effets indésirables tels qu'une complexité de code inutile, une surcharge inutile sur les performances ou même la création d'un nouvel anti-modèle.
Ce sont tous des éléments importants à prendre en compte lorsque vous envisagez d'appliquer un modèle de conception à notre code. Nous allons jeter un coup d'œil à certains des modèles de conception que j'ai personnellement trouvés utiles et je pense que chaque développeur JavaScript senior devrait être familiarisé avec.
Modèle de constructeur
Lorsque l'on pense aux langages orientés objet classiques, un constructeur est une fonction spéciale dans une classe qui initialise un objet avec un ensemble de valeurs par défaut et/ou envoyées.
Les méthodes courantes de création d'objets en JavaScript sont les trois suivantes :
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();Après avoir créé un objet, il existe quatre façons (depuis ES3) d'ajouter des propriétés à ces objets. Ce sont les suivants :
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });La manière la plus populaire de créer des objets est les accolades et, pour ajouter des propriétés, la notation par points ou les crochets. Toute personne ayant une expérience avec JavaScript les a utilisés.
Nous avons mentionné plus tôt que JavaScript ne prend pas en charge les classes natives, mais il prend en charge les constructeurs grâce à l'utilisation d'un « nouveau » mot-clé préfixé à un appel de fonction. De cette façon, nous pouvons utiliser la fonction comme constructeur et initialiser ses propriétés de la même manière que nous le ferions avec un constructeur de langage classique.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode(); Cependant, il y a encore place à l'amélioration ici. Si vous vous en souvenez, j'ai mentionné précédemment que JavaScript utilise l'héritage basé sur un prototype. Le problème avec l'approche précédente est que la méthode writesCode est redéfinie pour chacune des instances du constructeur Person . Nous pouvons éviter cela en définissant la méthode dans le prototype de la fonction :
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode(); Désormais, les deux instances du constructeur Person peuvent accéder à une instance partagée de la méthode writesCode() .

Modèle de module
En ce qui concerne les particularités, JavaScript ne cesse d'étonner. Une autre particularité de JavaScript (du moins en ce qui concerne les langages orientés objet) est que JavaScript ne prend pas en charge les modificateurs d'accès. Dans un langage POO classique, un utilisateur définit une classe et détermine les droits d'accès de ses membres. Étant donné que JavaScript dans sa forme simple ne prend en charge ni les classes ni les modificateurs d'accès, les développeurs JavaScript ont trouvé un moyen d'imiter ce comportement en cas de besoin.
Avant d'entrer dans les spécificités du modèle de module, parlons du concept de fermeture. Une fermeture est une fonction ayant accès à la portée parent, même après la fermeture de la fonction parent. Ils nous aident à imiter le comportement des modificateurs d'accès via la portée. Montrons cela via un exemple :
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());Comme vous pouvez le voir, en utilisant l'IIFE, nous avons lié la variable compteur à une fonction qui a été invoquée et fermée mais qui est toujours accessible par la fonction enfant qui l'incrémente. Comme nous ne pouvons pas accéder à la variable de compteur depuis l'extérieur de l'expression de la fonction, nous l'avons rendue privée grâce à une manipulation de la portée.
En utilisant les fermetures, nous pouvons créer des objets avec des parties privées et publiques. Ceux-ci sont appelés modules et sont très utiles chaque fois que nous voulons masquer certaines parties d'un objet et n'exposer qu'une interface à l'utilisateur du module. Montrons cela dans un exemple :
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());La chose la plus utile que ce modèle introduit est la séparation claire des parties privées et publiques d'un objet, qui est un concept très similaire aux développeurs issus d'un milieu orienté objet classique.
Cependant, tout n'est pas si parfait. Lorsque vous souhaitez modifier la visibilité d'un membre, vous devez modifier le code partout où vous avez utilisé ce membre en raison de la nature différente de l'accès aux parties publiques et privées. De plus, les méthodes ajoutées à l'objet après leur création ne peuvent pas accéder aux membres privés de l'objet.
Modèle de module révélateur
Ce modèle est une amélioration apportée au modèle de module tel qu'illustré ci-dessus. La principale différence est que nous écrivons toute la logique de l'objet dans la portée privée du module, puis exposons simplement les parties que nous voulons rendre publiques en renvoyant un objet anonyme. Nous pouvons également modifier la dénomination des membres privés lors du mappage des membres privés à leurs membres publics correspondants.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());Le modèle de module révélateur est l'une des trois manières au moins d'implémenter un modèle de module. Les différences entre le modèle de module révélateur et les autres variantes du modèle de module résident principalement dans la manière dont les membres publics sont référencés. En conséquence, le modèle de module révélateur est beaucoup plus facile à utiliser et à modifier ; cependant, il peut s'avérer fragile dans certains scénarios, comme l'utilisation d'objets RMP comme prototypes dans une chaîne d'héritage. Les situations problématiques sont les suivantes :
- Si nous avons une fonction privée qui fait référence à une fonction publique, nous ne pouvons pas remplacer la fonction publique, car la fonction privée continuera à faire référence à l'implémentation privée de la fonction, introduisant ainsi un bogue dans notre système.
- Si nous avons un membre public pointant vers une variable privée et essayons de remplacer le membre public depuis l'extérieur du module, les autres fonctions feraient toujours référence à la valeur privée de la variable, introduisant un bogue dans notre système.
Modèle de singleton
Le modèle singleton est utilisé dans les scénarios où nous avons besoin d'exactement une instance d'une classe. Par exemple, nous avons besoin d'un objet qui contient une configuration pour quelque chose. Dans ces cas, il n'est pas nécessaire de créer un nouvel objet chaque fois que l'objet de configuration est requis quelque part dans le système.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);Comme vous pouvez le voir dans l'exemple, le nombre aléatoire généré est toujours le même, ainsi que les valeurs de configuration envoyées.
Il est important de noter que le point d'accès pour récupérer la valeur du singleton doit être unique et très bien connu. Un inconvénient à l'utilisation de ce modèle est qu'il est plutôt difficile à tester.
Modèle d'observateur
Le modèle d'observateur est un outil très utile lorsque nous avons un scénario dans lequel nous devons améliorer la communication entre des parties disparates de notre système de manière optimisée. Il favorise le couplage lâche entre les objets.
Il existe différentes versions de ce modèle, mais dans sa forme la plus basique, nous avons deux parties principales du modèle. Le premier est un sujet et le second est des observateurs.
Un sujet gère toutes les opérations concernant un certain sujet auquel les observateurs souscrivent. Ces opérations inscrivent un observateur à un certain sujet, désabonnent un observateur d'un certain sujet et informent les observateurs d'un certain sujet lorsqu'un événement est publié.
Cependant, il existe une variante de ce modèle appelée modèle éditeur/abonné, que je vais utiliser comme exemple dans cette section. La principale différence entre un modèle d'observateur classique et le modèle éditeur/abonné est que l'éditeur/abonné favorise un couplage encore plus lâche que le modèle observateur.
Dans le modèle observateur, le sujet détient les références aux observateurs abonnés et appelle les méthodes directement à partir des objets eux-mêmes tandis que, dans le modèle éditeur/abonné, nous avons des canaux, qui servent de pont de communication entre un abonné et un éditeur. L'éditeur déclenche un événement et exécute simplement la fonction de rappel envoyée pour cet événement.
Je vais afficher un court exemple du modèle éditeur/abonné, mais pour les personnes intéressées, un exemple de modèle d'observateur classique peut être facilement trouvé en ligne.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});Ce modèle de conception est utile dans les situations où nous devons effectuer plusieurs opérations sur un seul événement déclenché. Imaginez que vous ayez un scénario dans lequel nous devons effectuer plusieurs appels AJAX vers un service principal, puis effectuer d'autres appels AJAX en fonction du résultat. Vous auriez à imbriquer les appels AJAX les uns dans les autres, entrant éventuellement dans une situation connue sous le nom d'enfer de rappel. L'utilisation du modèle éditeur/abonné est une solution beaucoup plus élégante.
Un inconvénient de l'utilisation de ce modèle est la difficulté de tester différentes parties de notre système. Il n'existe aucun moyen élégant pour nous de savoir si les parties abonnées du système se comportent ou non comme prévu.
Modèle de médiateur
Nous aborderons brièvement un modèle qui est également très utile lorsque l'on parle de systèmes découplés. Lorsque nous avons un scénario où plusieurs parties d'un système doivent communiquer et être coordonnées, une bonne solution serait peut-être d'introduire un médiateur.
Un médiateur est un objet utilisé comme point central de communication entre des parties disparates d'un système et gère le flux de travail entre elles. Maintenant, il est important de souligner qu'il gère le flux de travail. Pourquoi est-ce important?
Parce qu'il y a une grande similitude avec le modèle éditeur/abonné. Vous pourriez vous demander, OK, donc ces deux modèles aident tous les deux à mettre en œuvre une meilleure communication entre les objets… Quelle est la différence ?
La différence est qu'un médiateur gère le flux de travail, tandis que l'éditeur/abonné utilise ce qu'on appelle un type de communication "tire et oublie". L'éditeur/abonné est simplement un agrégateur d'événements, ce qui signifie qu'il s'occupe simplement de déclencher les événements et de faire savoir aux bons abonnés quels événements ont été déclenchés. L'agrégateur d'événements ne se soucie pas de ce qui se passe une fois qu'un événement a été déclenché, ce qui n'est pas le cas avec un médiateur.
Un bel exemple de médiateur est une interface de type assistant. Disons que vous avez un long processus d'enregistrement pour un système sur lequel vous avez travaillé. Souvent, lorsque de nombreuses informations sont requises d'un utilisateur, il est recommandé de les décomposer en plusieurs étapes.
De cette façon, le code sera beaucoup plus propre (plus facile à entretenir) et l'utilisateur n'est pas submergé par la quantité d'informations demandées juste pour terminer l'enregistrement. Un médiateur est un objet qui gérerait les étapes d'enregistrement, en tenant compte des différents flux de travail possibles qui pourraient se produire en raison du fait que chaque utilisateur pourrait potentiellement avoir un processus d'enregistrement unique.
L'avantage évident de ce modèle de conception est une meilleure communication entre les différentes parties d'un système, qui communiquent désormais toutes via le médiateur et la base de code plus propre.
Un inconvénient serait que nous avons maintenant introduit un point de défaillance unique dans notre système, ce qui signifie que si notre médiateur tombe en panne, l'ensemble du système pourrait cesser de fonctionner.
Modèle prototype
Comme nous l'avons déjà mentionné tout au long de l'article, JavaScript ne prend pas en charge les classes dans sa forme native. L'héritage entre objets est implémenté à l'aide d'une programmation basée sur des prototypes.
Il nous permet de créer des objets qui peuvent servir de prototype pour d'autres objets en cours de création. L'objet prototype est utilisé comme modèle pour chaque objet créé par le constructeur.
Comme nous en avons déjà parlé dans les sections précédentes, montrons un exemple simple de la façon dont ce modèle pourrait être utilisé.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
Prochaines étapes
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.
