Lorsque vous créez des applications JavaScript, vous pouvez rencontrer des scénarios dans lesquels vous devez construire des objets d’une certaine manière, prédéfinie, ou réutiliser une classe commune en la modifiant ou en l’adaptant à plusieurs cas d’utilisation.

Bien entendu, il n’est pas pratique de résoudre ces problèmes encore et encore.

C’est là que les modèles de conception JavaScript viennent à votre secours.

Les modèles de conception JavaScript vous offrent une méthode structurée et reproductible pour résoudre les problèmes courants du développement JavaScript.

Dans ce guide, nous allons examiner ce que sont les modèles de conception JavaScript et comment les utiliser dans vos applications JavaScript.

Qu’est-ce qu’un modèle de conception JavaScript ?

Les modèles de conception JavaScript sont des solutions de modèles répétables pour les problèmes fréquents dans le développement d’applications JavaScript.

L’idée est simple : Les programmeurs du monde entier, depuis l’aube du développement, ont été confrontés à des séries de problèmes récurrents lors du développement d’applications. Au fil du temps, certains développeurs ont choisi de documenter des méthodes éprouvées pour résoudre ces problèmes afin que d’autres puissent se référer facilement à ces solutions.

Comme de plus en plus de développeurs ont choisi d’utiliser ces solutions et ont reconnu leur efficacité à résoudre leurs problèmes, elles ont été acceptées comme une manière standard de résoudre les problèmes et ont reçu le nom de « design patterns » ou modèles de conception.

Au fur et à mesure que l’importance des modèles de conception a été mieux comprise, ceux-ci ont été développés et normalisés. La plupart des modèles de conception modernes ont désormais une structure définie, sont organisés en plusieurs catégories et sont enseignés dans les diplômes liés à l’informatique en tant que sujets indépendants.

Types de modèles de conception JavaScript

Voici quelques-unes des classifications les plus populaires des modèles de conception JavaScript.

Créatif

Les modèles de conception créatifs sont ceux qui aident à résoudre les problèmes liés à la création et à la gestion de nouvelles instances d’objets en JavaScript. Cela peut être aussi simple que de limiter une classe à un seul objet ou aussi complexe que de définir une méthode complexe de sélection et d’ajout de chaque fonctionnalité dans un objet JavaScript.

Parmi les exemples de modèles de conception créatifs, citons Singleton, Factory, Abstract Factory et Builder, entre autres.

Structurel

Les patrons de conception structurels sont ceux qui aident à résoudre les problèmes liés à la gestion de la structure (ou schéma) des objets JavaScript. Ces problèmes peuvent inclure la création d’une relation entre deux objets différents ou l’abstraction de certaines caractéristiques d’un objet pour des utilisateurs spécifiques.

Parmi les exemples de patrons de conception structurels, citons Adapter, Bridge, Composite et Facade.

Comportemental

Les modèles de conception comportementaux sont ceux qui aident à résoudre les problèmes liés à la façon dont le contrôle (et la responsabilité) est transmis entre divers objets. Ces problèmes peuvent concerner le contrôle de l’accès à une liste chainée ou l’établissement d’une entité unique qui peut contrôler l’accès à plusieurs types d’objets.

Parmi les exemples de patrons de conception comportementaux, citons Command, Iterator, Memento et Observer.

Concussion

Les modèles de conception de concussion sont ceux qui aident à résoudre les problèmes liés au multi-threading et au multitâche. Ces problèmes peuvent consister à maintenir un objet actif parmi plusieurs objets disponibles ou à traiter plusieurs évènements fournis à un système en démultiplexant les entrées entrantes et en les traitant pièce par pièce.

Parmi les exemples de modèles de concussion, citons l’objet actif, la réaction nucléaire et l’ordonnanceur.

Architectural

Les modèles de conception architecturale sont ceux qui aident à résoudre les problèmes liés à la conception de logiciels au sens large. Ils sont généralement liés à la manière de concevoir votre système et d’assurer une haute disponibilité, d’atténuer les risques et d’éviter les goulots d’étranglement en matière de performances.

Deux exemples de modèles de conception architecturale sont MVC et MVVM.

Éléments d’un modèle de conception

Presque tous les modèles de conception peuvent être décomposés en un ensemble de quatre composants importants. Il s’agit de :

  • Nom du modèle : il est utilisé pour identifier un modèle de conception tout en communiquant avec d’autres utilisateurs. Les exemples incluent « singleton », « prototype », et plus encore.
  • Problème : il décrit l’objectif du modèle de conception. Il s’agit d’une petite description du problème que le modèle de conception tente de résoudre. Il peut même inclure un exemple de scénario pour mieux expliquer le problème. Il peut également contenir une liste de conditions à remplir pour qu’un modèle de conception résolve entièrement le problème sous-jacent.
  • Solution : il s’agit de la solution au problème posé, composée d’éléments tels que des classes, des méthodes, des interfaces, etc. C’est là que réside l’essentiel d’un modèle de conception – il implique des relations, des responsabilités et des collaborateurs de divers éléments qui sont clairement définis.
  • Résultats : il s’agit d’une analyse de la façon dont le modèle a permis de résoudre le problème. Des choses comme l’utilisation de l’espace et du temps sont discutées, ainsi que les approches alternatives pour résoudre le même problème.

Si vous souhaitez en savoir plus sur les modèles de conception et leur création, MSU propose un matériel d’étude succinct auquel vous pouvez vous référer.

Pourquoi devriez-vous utiliser les modèles de conception ?

Il existe de multiples raisons pour lesquelles vous souhaiteriez utiliser des modèles de conception :

  • Ils sont éprouvés et testés : avec un modèle de conception, vous disposez d’une solution éprouvée à votre problème (pour autant que le modèle de conception corresponde à la description de votre problème). Vous ne devez pas perdre de temps à chercher des solutions alternatives, et vous pouvez être sûr que vous avez une solution qui s’occupe de l’optimisation de base des performances pour vous.
  • Ils sont faciles à comprendre : les modèles de conception sont conçus pour être petits, simples et faciles à comprendre. Vous n’avez pas besoin d’être un programmeur spécialisé travaillant dans un secteur spécifique depuis des décennies pour comprendre quel modèle de conception utiliser. Ils sont volontairement génériques (ils ne se limitent pas à un langage de programmation particulier) et peuvent être compris par toute personne possédant des compétences suffisantes en matière de résolution de problèmes. Cela est également utile lorsque vous changez de personnes dans votre équipe technique : Un morceau de code qui s’appuie sur un modèle de conception est plus facile à comprendre pour tout nouveau développeur de logiciels.
  • Ils sont simples à mettre en œuvre : la plupart des modèles de conception sont très simples, comme vous le verrez plus loin dans notre article. Vous n’avez pas besoin de connaitre de multiples concepts de programmation pour les mettre en œuvre dans votre code.
  • Ils proposent une architecture de code facilement réutilisable : La réutilisabilité et la propreté du code sont fortement encouragées dans l’industrie technologique, et les modèles de conception peuvent vous aider à y parvenir. Étant donné que ces modèles sont une façon standard de résoudre les problèmes, leurs concepteurs ont pris soin de s’assurer que l’architecture de l’application englobante reste réutilisable, flexible et compatible avec la plupart des formes d’écriture de code.
  • Ils permettent de gagner du temps et de réduire la taille de l’application : L’un des plus grands avantages de s’appuyer sur un ensemble standard de solutions est qu’elles vous permettront de gagner du temps lors de leur mise en œuvre. Il y a de fortes chances que toute votre équipe de développement connaisse bien les modèles de conception, il sera donc plus facile pour elle de planifier, de communiquer et de collaborer lors de leur mise en œuvre. Les solutions éprouvées et testées signifient qu’il y a de bonnes chances que vous ne finissiez pas par perdre des ressources ou faire un détour lors de la création d’une fonctionnalité, ce qui vous fait gagner du temps et de l’espace. En outre, la plupart des langages de programmation vous fournissent des bibliothèques de modèles standard qui mettent déjà en œuvre certains modèles de conception courants comme Iterator et Observer.

Les 20 principaux modèles de conception JavaScript à maitriser

Maintenant que vous comprenez de quoi est fait un modèle de conception et pourquoi vous en avez besoin, plongeons plus profondément dans la façon dont certains des modèles de conception JavaScript les plus couramment utilisés peuvent être mis en œuvre dans une application JavaScript.

Création

Commençons par quelques modèles de conception créatifs fondamentaux et faciles à apprendre.

1. Singleton

Le modèle Singleton est l’un des modèles de conception les plus utilisés dans le secteur du développement logiciel. Le problème qu’il vise à résoudre est de ne maintenir qu’une seule instance d’une classe. Cela peut s’avérer pratique lors de l’instanciation d’objets gourmands en ressources, comme les gestionnaires de bases de données.

Voici comment vous pouvez l’implémenter en JavaScript :

function SingletonFoo() {

   let fooInstance = null;

   // For our reference, let's create a counter that will track the number of active instances
   let count = 0;

   function printCount() {
       console.log("Number of instances: " + count);
   }

   function init() {
       // For our reference, we'll increase the count by one whenever init() is called
       count++;

       // Do the initialization of the resource-intensive object here and return it
       return {}
   }

   function createInstance() {
       if (fooInstance == null) {
           fooInstance = init();
       }
       return fooInstance;
   }

   function closeInstance() {
       count--;
       fooInstance = null;
   }

   return {
       initialize: createInstance,
       close: closeInstance,
       printCount: printCount
   }
}

let foo = SingletonFoo();

foo.printCount() // Prints 0
foo.initialize()
foo.printCount() // Prints 1
foo.initialize()
foo.printCount() // Still prints 1
foo.initialize()
foo.printCount() // Still 1
foo.close()
foo.printCount() // Prints 0

Bien qu’il remplisse bien son rôle, le modèle Singleton est connu pour rendre le débogage difficile car il masque les dépendances et contrôle l’accès à l’initialisation ou à la destruction des instances d’une classe.

2. Factory

La méthode Factory est également l’un des modèles de conception les plus populaires. Le problème que la méthode Factory vise à résoudre est la création d’objets sans utiliser le constructeur conventionnel. Au lieu de cela, elle prend en compte la configuration (ou la description) de l’objet que vous souhaitez et renvoie l’objet nouvellement créé.

Voici comment vous pouvez l’implémenter en JavaScript :

function Factory() {
   this.createDog = function (breed) {
       let dog;

       if (breed === "labrador") {
           dog = new Labrador();
       } else if (breed === "bulldog") {
           dog = new Bulldog();
       } else if (breed === "golden retriever") {
           dog = new GoldenRetriever();
       } else if (breed === "german shepherd") {
           dog = new GermanShepherd();
       }

       dog.breed = breed;
       dog.printInfo = function () {
           console.log("nnBreed: " + dog.breed + "nShedding Level (out of 5): " + dog.sheddingLevel + "nCoat Length: " + dog.coatLength + "nCoat Type: " + dog.coatType)
       }

       return dog;
   }
}

function Labrador() {
   this.sheddingLevel = 4
   this.coatLength = "short"
   this.coatType = "double"
}

function Bulldog() {
   this.sheddingLevel = 3
   this.coatLength = "short"
   this.coatType = "smooth"
}

function GoldenRetriever() {
   this.sheddingLevel = 4
   this.coatLength = "medium"
   this.coatType = "double"
}

function GermanShepherd() {
   this.sheddingLevel = 4
   this.coatLength = "medium"
   this.coatType = "double"
}

function run() {

   let dogs = [];
   let factory = new Factory();

   dogs.push(factory.createDog("labrador"));
   dogs.push(factory.createDog("bulldog"));
   dogs.push(factory.createDog("golden retriever"));
   dogs.push(factory.createDog("german shepherd"));

   for (var i = 0, len = dogs.length; i < len; i++) {
       dogs[i].printInfo();
   }
}

run()

/**
Output:

Breed: labrador
Shedding Level (out of 5): 4
Coat Length: short
Coat Type: double


Breed: bulldog
Shedding Level (out of 5): 3
Coat Length: short
Coat Type: smooth


Breed: golden retriever
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double


Breed: german shepherd
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double
*/

Le modèle de conception Factory contrôle la manière dont les objets seront créés et vous fournit un moyen rapide de créer de nouveaux objets, ainsi qu’une interface uniforme qui définit les propriétés que vos objets auront. Vous pouvez ajouter autant de races de chiens que vous le souhaitez, mais tant que les méthodes et les propriétés exposées par les types de races restent les mêmes, elles fonctionneront parfaitement.

Toutefois, notez que le modèle Factory peut souvent conduire à un grand nombre de classes qui peuvent être difficiles à gérer.

3. Abstract Factory

La méthode Abstract Factory fait passer la méthode Factory à un niveau supérieur en rendant les fabriques abstraites et donc remplaçables sans que l’environnement appelant ne connaisse la fabrique exacte utilisée ou son fonctionnement interne. L’environnement appelant sait seulement que toutes les fabriques ont un ensemble de méthodes communes qu’il peut appeler pour effectuer l’action d’instanciation.

C’est ainsi que l’on peut l’implémenter en utilisant l’exemple précédent :

// A factory to create dogs
function DogFactory() {
   // Notice that the create function is now createPet instead of createDog, since we need
   // it to be uniform across the other factories that will be used with this
   this.createPet = function (breed) {
       let dog;

       if (breed === "labrador") {
           dog = new Labrador();
       } else if (breed === "pug") {
           dog = new Pug();
       }

       dog.breed = breed;
       dog.printInfo = function () {
           console.log("nnType: " + dog.type + "nBreed: " + dog.breed + "nSize: " + dog.size)
       }

       return dog;
   }
}

// A factory to create cats
function CatFactory() {
   this.createPet = function (breed) {
       let cat;

       if (breed === "ragdoll") {
           cat = new Ragdoll();
       } else if (breed === "singapura") {
           cat = new Singapura();
       }

       cat.breed = breed;
       cat.printInfo = function () {
           console.log("nnType: " + cat.type + "nBreed: " + cat.breed + "nSize: " + cat.size)
       }

       return cat;
   }
}

// Dog and cat breed definitions
function Labrador() {
   this.type = "dog"
   this.size = "large"
}

function Pug() {
   this.type = "dog"
   this.size = "small"
}

function Ragdoll() {
   this.type = "cat"
   this.size = "large"
}

function Singapura() {
   this.type = "cat"
   this.size = "small"
}

function run() {

   let pets = [];

   // Initialize the two factories
   let catFactory = new CatFactory();
   let dogFactory = new DogFactory();

   // Create a common petFactory that can produce both cats and dogs
   // Set it to produce dogs first
   let petFactory = dogFactory;

   pets.push(petFactory.createPet("labrador"));
   pets.push(petFactory.createPet("pug"));

   // Set the petFactory to produce cats
   petFactory = catFactory;

   pets.push(petFactory.createPet("ragdoll"));
   pets.push(petFactory.createPet("singapura"));

   for (var i = 0, len = pets.length; i < len; i++) {
       pets[i].printInfo();
   }
}

run()

/**
Output:

Type: dog
Breed: labrador
Size: large


Type: dog
Breed: pug
Size: small


Type: cat
Breed: ragdoll
Size: large


Type: cat
Breed: singapura
Size: small

*/

Le patron Abstract Factory vous permet d’échanger facilement des fabriques concrètes, et il contribue à promouvoir l’uniformité entre les fabriques et les produits créés. Cependant, il peut devenir difficile d’introduire de nouveaux types de produits, car vous devrez apporter des modifications à plusieurs classes pour accueillir de nouvelles méthodes/propriétés.

4. Builder

Le modèle Builder est l’un des modèles de conception JavaScript de création les plus complexes mais aussi les plus flexibles. Il vous permet de construire chaque fonctionnalité de votre produit une par une, vous offrant un contrôle total sur la façon dont votre objet est construit tout en faisant abstraction des détails internes.

Dans l’exemple complexe ci-dessous, vous verrez le modèle de conception Builder en action ainsi que le directeur qui vous aidera à fabriquer des pizzas !

// Here's the PizzaBuilder (you can also call it the chef)
function PizzaBuilder() {
   let base
   let sauce
   let cheese
   let toppings = []

   // The definition of pizza is hidden from the customers
   function Pizza(base, sauce, cheese, toppings) {
       this.base = base
       this.sauce = sauce
       this.cheese = cheese
       this.toppings = toppings

       this.printInfo = function() {
           console.log("This pizza has " + this.base + " base with " + this.sauce + " sauce "
           + (this.cheese !== undefined ? "with cheese. " : "without cheese. ")
           + (this.toppings.length !== 0 ? "It has the following toppings: " + toppings.toString() : ""))
       }
   }

   // You can request the PizzaBuilder (/chef) to perform any of the following actions on your pizza
   return {
       addFlatbreadBase: function() {
           base = "flatbread"
           return this;
       },
       addTomatoSauce: function() {
           sauce = "tomato"
           return this;
       },
       addAlfredoSauce: function() {
           sauce = "alfredo"
           return this;
       },
       addCheese: function() {
           cheese = "parmesan"
           return this;
       },
       addOlives: function() {
           toppings.push("olives")
           return this
       },
       addJalapeno: function() {
           toppings.push("jalapeno")
           return this
       },
       cook: function() {
           if (base === null){
               console.log("Can't make a pizza without a base")
               return
           }
           return new Pizza(base, sauce, cheese, toppings)
       }
   }

}

// This is the Director for the PizzaBuilder, aka the PizzaShop.
// It contains a list of preset steps that can be used to prepare common pizzas (aka recipes!)
function PizzaShop() {
   return {
       makePizzaMargherita: function() {
           pizzaBuilder = new PizzaBuilder()
           pizzaMargherita = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().cook()
           return pizzaMargherita
       },
       makePizzaAlfredo: function() {
           pizzaBuilder = new PizzaBuilder()
           pizzaAlfredo = pizzaBuilder.addFlatbreadBase().addAlfredoSauce().addCheese().addJalapeno().cook()
           return pizzaAlfredo
       },
       makePizzaMarinara: function() {
           pizzaBuilder = new PizzaBuilder()
           pizzaMarinara = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addOlives().cook()
           return pizzaMarinara
       }
   }
}

// Here's where the customer can request pizzas from
function run() {

   let pizzaShop = new PizzaShop()

   // You can ask for one of the popular pizza recipes...
   let pizzaMargherita = pizzaShop.makePizzaMargherita()
   pizzaMargherita.printInfo()
   // Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives

   let pizzaAlfredo = pizzaShop.makePizzaAlfredo()
   pizzaAlfredo.printInfo()
   // Output: This pizza has flatbread base with alfredo sauce with cheese. It has the following toppings: jalapeno

   let pizzaMarinara = pizzaShop.makePizzaMarinara()
   pizzaMarinara.printInfo()
   // Output: This pizza has flatbread base with tomato sauce without cheese. It has the following toppings: olives

   // Or send your custom request directly to the chef!
   let chef = PizzaBuilder()
   let customPizza = chef.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().addJalapeno().cook()
   customPizza.printInfo()
   // Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives,jalapeno

}

run()

Vous pouvez associer le Builder à un Director, comme le montre la classe PizzaShop dans l’exemple ci-dessus, pour prédéfinir un ensemble d’étapes à suivre à chaque fois pour construire une variante standard de votre produit, c’est-à-dire une recette spécifique pour vos pizzas.

Le seul problème de ce modèle de conception est qu’il est assez complexe à mettre en place et à maintenir. Cependant, l’ajout de nouvelles fonctionnalités de cette façon est plus simple que la méthode Factory.

5. Prototype

Le modèle de conception Prototype est un moyen rapide et simple de créer de nouveaux objets à partir d’objets existants en les clonant.

Un objet prototype est d’abord créé, qui peut être cloné plusieurs fois pour créer de nouveaux objets. Il s’avère pratique lorsque l’instanciation directe d’un objet est une opération plus gourmande en ressources que la création d’une copie d’un objet existant.

Dans l’exemple ci-dessous, vous verrez comment vous pouvez utiliser le modèle Prototype pour créer de nouveaux documents basés sur un document modèle défini :

// Defining how a document would look like
function Document() {
   this.header = "Acme Co"
   this.footer = "For internal use only"
   this.pages = 2
   this.text = ""
  
   this.addText = function(text) {
       this.text += text
   }

   // Method to help you see the contents of the object
   this.printInfo = function() {
       console.log("nnHeader: " + this.header + "nFooter: " + this.footer + "nPages: " + this.pages + "nText: " + this.text)
   }

  
}

// A protype (or template) for creating new blank documents with boilerplate information
function DocumentPrototype(baseDocument) {
   this.baseDocument = baseDocument
  
   // This is where the magic happens. A new document object is created and is assigned the values of the current object
   this.clone = function() {
       let document = new Document();

       document.header = this.baseDocument.header
       document.footer = this.baseDocument.footer
       document.pages = this.baseDocument.pages
       document.text = this.baseDocument.text

       return document
   }
}

function run() {
   // Create a document to use as the base for the prototype
   let baseDocument = new Document()

   // Make some changes to the prototype
   baseDocument.addText("This text was added before cloning and will be common in both documents. ")

   let prototype = new DocumentPrototype(baseDocument)

   // Create two documents from the prototype
   let doc1 = prototype.clone()
   let doc2 = prototype.clone()

   // Make some changes to both objects
   doc1.pages = 3

   doc1.addText("This is document 1")
   doc2.addText("This is document 2")

   // Print their values
   doc1.printInfo()
   /* Output:
       Header: Acme Co
       Footer: For internal use only
       Pages: 3
       Text: This text was added before cloning and will be common in both documents. This is document 1
    */

   doc2.printInfo()
   /** Output:
       Header: Acme Co
       Footer: For internal use only
       Pages: 2
       Text: This text was added before cloning and will be common in both documents. This is document 2
    */
}

run()

La méthode Prototype fonctionne très bien dans les cas où une grande partie de vos objets partagent les mêmes valeurs, ou lorsque la création d’un nouvel objet est assez coûteuse. Cependant, elle semble exagérée dans les cas où vous n’avez pas besoin de plus de quelques instances de la classe.

Structurel

Les modèles de conception structurels vous aident à organiser votre logique métier en vous fournissant des moyens éprouvés de structurer vos classes. Il existe une variété de modèles de conception structurels qui répondent chacun à des cas d’utilisation uniques.

6. Adapter

Un problème courant lors de la création d’applications est de permettre la collaboration entre des classes incompatibles.

Un bon exemple pour comprendre cela est de maintenir la compatibilité ascendante. Si vous écrivez une nouvelle version d’une classe, vous voudriez naturellement qu’elle soit facilement utilisable à tous les endroits où l’ancienne version fonctionnait. Toutefois, si vous effectuez des modifications de rupture, comme la suppression ou la mise à jour de méthodes qui étaient cruciales pour le fonctionnement de l’ancienne version, vous risquez de vous retrouver avec une classe dont tous les clients doivent être mis à jour pour pouvoir être exécutés.

Dans ce cas, le modèle de conception Adapter peut vous aider.

Le modèle de conception Adapter vous fournit une abstraction qui comble le fossé entre les méthodes et propriétés de la nouvelle classe et celles de l’ancienne. Elle possède la même interface que l’ancienne classe, mais elle contient une logique permettant de faire correspondre les anciennes méthodes aux nouvelles pour exécuter des opérations similaires. C’est similaire à la façon dont une prise électrique fait office d’adaptateur entre une prise de style américain et une prise de style européen.

Voici un exemple :

// Old bot
function Robot() {

   this.walk = function(numberOfSteps) {
       // code to make the robot walk
       console.log("walked " + numberOfSteps + " steps")
   }

   this.sit = function() {
       // code to make the robot sit
       console.log("sit")
   }

}

// New bot that does not have the walk function anymore
// but instead has functions to control each step independently
function AdvancedRobot(botName) {
   // the new bot has a name as well
   this.name = botName

   this.sit = function() {
       // code to make the robot sit
       console.log("sit")
   }

   this.rightStepForward = function() {
       // code to take 1 step from right leg forward
       console.log("right step forward")
   }

   this.leftStepForward = function () {
       // code to take 1 step from left leg forward
       console.log("left step forward")
   }
}

function RobotAdapter(botName) {
   // No references to the old interfact since that is usually
   // phased out of development
   const robot = new AdvancedRobot(botName)

   // The adapter defines the walk function by using the
   // two step controls. You now have room to choose which leg to begin/end with,
   // and do something at each step.
   this.walk = function(numberOfSteps) {
       for (let i=0; i<numberOfSteps; i++) {
          
           if (i % 2 === 0) {
               robot.rightStepForward()
           } else {
               robot.leftStepForward()
           }
       }
   }

   this.sit = robot.sit

}

function run() {

   let robot = new Robot()

   robot.sit()
   // Output: sit
   robot.walk(5)
   // Output: walked 5 steps

   robot = new RobotAdapter("my bot")

   robot.sit()
   // Output: sit
   robot.walk(5)
   // Output:
   // right step forward
   // left step forward
   // right step forward
   // left step forward
   // right step forward

}

run()

Le principal problème de ce modèle de conception est qu’il ajoute de la complexité à votre code source. Vous deviez déjà maintenir deux classes différentes, et maintenant vous avez une autre classe – l’adaptateur – à maintenir.

7. Bridge

Développant le modèle Adapter, le modèle Bridge fournit à la fois la classe et le client avec des interfaces séparées afin qu’ils puissent tous deux fonctionner même en cas d’interfaces natives incompatibles.

Il permet de développer une interface à couplage très lâche entre les deux types d’objets. Il permet également d’améliorer l’extensibilité des interfaces et de leurs implémentations pour une flexibilité maximale.

Voici comment vous pouvez l’utiliser :

// The TV and speaker share the same interface
function TV() {
   this.increaseVolume = function() {
       // logic to increase TV volume
   }

   this.decreaseVolume = function() {
       // logic to decrease TV volume
   }

   this.mute = function() {
       // logic to mute TV audio
   }
}

function Speaker() {
   this.increaseVolume = function() {
       // logic to increase speaker volume
   }

   this.decreaseVolume = function() {
       // logic to decrease speaker volume
   }

   this.mute() = function() {
       // logic to mute speaker audio
   }
}

// The two remotes make use of the same common interface
// that supports volume up and volume down features
function SimpleRemote(device) {
   this.pressVolumeDownKey = function() {
       device.decreaseVolume()
   }

   this.pressVolumeUpKey = function() {
       device.increaseVolume()
   }
}

function AdvancedRemote(device) {

   this.pressVolumeDownKey = function() {
       device.decreaseVolume()
   }

   this.pressVolumeUpKey = function() {
       device.increaseVolume()
   }

   this.pressMuteKey = function() {
       device.mute()
   }
}

function run() {

   let tv = new TV()
   let speaker = new Speaker()

   let tvSimpleRemote = new SimpleRemote(tv)
   let tvAdvancedRemote = new AdvancedRemote(tv)

   let speakerSimpleRemote = new SimpleRemote(speaker)
   let speakerAdvancedRemote = new AdvancedRemote(speaker)

   // The methods listed in pair below will have the same effect
   // on their target devices
   tvSimpleRemote.pressVolumeDownKey()
   tvAdvancedRemote.pressVolumeDownKey()

   tvSimpleRemote.pressVolumeUpKey()
   tvAdvancedRemote.pressVolumeUpKey()

   // The advanced remote has additional functionality
   tvAdvancedRemote.pressMuteKey()

   speakerSimpleRemote.pressVolumeDownKey()
   speakerAdvancedRemote.pressVolumeDownKey()

   speakerSimpleRemote.pressVolumeUpKey()
   speakerAdvancedRemote.pressVolumeUpKey()

   speakerAdvancedRemote.pressMuteKey()
}

Comme vous l’avez peut-être déjà deviné, le modèle Bridge augmente considérablement la complexité de la base de code. De plus, la plupart des interfaces n’ont généralement qu’une seule implémentation dans les cas d’utilisation réels, de sorte que vous ne bénéficiez pas vraiment de la réutilisabilité du code.

8. Composite

Le modèle de conception Composite vous aide à structurer et à gérer facilement des objets et des entités similaires. L’idée de base du modèle Composite est que les objets et leurs conteneurs logiques peuvent être représentés à l’aide d’une seule classe abstraite (qui peut stocker les données/méthodes liées à l’objet et les références à elle-même pour le conteneur).

Il est plus logique d’utiliser le modèle Composite lorsque votre modèle de données ressemble à une structure arborescente. Cependant, vous ne devriez pas essayer de transformer un modèle de données non arborescent en un modèle de données arborescent juste pour utiliser le motif Composite, car cela peut souvent vous faire perdre beaucoup de flexibilité.

Dans l’exemple ci-dessous, vous verrez comment utiliser le modèle de conception Composite pour construire un système d’emballage pour les produits de commerce électronique qui peut également calculer la valeur totale de la commande par emballage :

// A product class, that acts as a Leaf node
function Product(name, price) {
   this.name = name
   this.price = price

   this.getTotalPrice = function() {
       return this.price
   }
}

// A box class, that acts as a parent/child node
function Box(name) {
   this.contents = []
   this.name = name

   // Helper function to add an item to the box
   this.add = function(content){
       this.contents.push(content)
   }

   // Helper function to remove an item from the box
   this.remove = function() {
       var length = this.contents.length;
       for (var i = 0; i < length; i++) {
           if (this.contents[i] === child) {
               this.contents.splice(i, 1);
               return;
           }
       }
   }

   // Helper function to get one item from the box
   this.getContent = function(position) {
       return this.contents[position]
   }

   // Helper function to get the total count of the items in the box
   this.getTotalCount = function() {
       return this.contents.length
   }

   // Helper function to calculate the total price of all items in the box
   this.getTotalPrice = function() {
       let totalPrice = 0;

       for (let i=0; i < this.getTotalCount(); i++){
           totalPrice += this.getContent(i).getTotalPrice()
       }

       return totalPrice
   }
}

function run() {

   // Let's create some electronics
   const mobilePhone = new Product("mobile phone," 1000)
   const phoneCase = new Product("phone case," 30)
   const screenProtector = new Product("screen protector," 20)

   // and some stationery products
   const pen = new Product("pen," 2)
   const pencil = new Product("pencil," 0.5)
   const eraser = new Product("eraser," 0.5)
   const stickyNotes = new Product("sticky notes," 10)

   // and put them in separate boxes
   const electronicsBox = new Box("electronics")
   electronicsBox.add(mobilePhone)
   electronicsBox.add(phoneCase)
   electronicsBox.add(screenProtector)
  
   const stationeryBox = new Box("stationery")
   stationeryBox.add(pen)
   stationeryBox.add(pencil)
   stationeryBox.add(eraser)
   stationeryBox.add(stickyNotes)

   // and finally, put them into one big box for convenient shipping
   const package = new Box('package')
   package.add(electronicsBox)
   package.add(stationeryBox)

   // Here's an easy way to calculate the total order value
   console.log("Total order price: USD " + package.getTotalPrice())
   // Output: USD 1063
}

run()

Le plus gros inconvénient de l’utilisation du modèle Composite est que les modifications des interfaces des composants peuvent être très difficiles à l’avenir. La conception des interfaces demande du temps et des efforts, et la nature arborescente du modèle de données peut rendre très difficile d’apporter des modifications comme vous le souhaitez.

9. Decorator

Le modèle Decorator vous aide à ajouter de nouvelles fonctionnalités aux objets existants en les enveloppant simplement dans un nouvel objet. C’est un peu comme si vous pouviez emballer une boîte cadeau déjà emballée avec un nouveau papier d’emballage autant de fois que vous le souhaitez : Chaque emballage vous permet d’ajouter autant de fonctionnalités que vous le souhaitez. C’est donc une excellente solution en termes de flexibilité.

D’un point de vue technique, aucun héritage n’est impliqué, ce qui offre une plus grande liberté lors de la conception de la logique commerciale.

Dans l’exemple ci-dessous, vous verrez comment le modèle Decorator permet d’ajouter plus de fonctionnalités à une classe standard Customer:

function Customer(name, age) {
   this.name = name
   this.age = age

   this.printInfo = function() {
       console.log("Customer:nName : " + this.name + " | Age: " + this.age)
   }
}

function DecoratedCustomer(customer, location) {
   this.customer = customer
   this.name = customer.name
   this.age = customer.age
   this.location = location

   this.printInfo = function() {
       console.log("Decorated Customer:nName: " + this.name + " | Age: " + this.age + " | Location: " + this.location)
   }
}

function run() {
   let customer = new Customer("John," 25)
   customer.printInfo()
   // Output:
   // Customer:
   // Name : John | Age: 25

   let decoratedCustomer = new DecoratedCustomer(customer, "FL")
   decoratedCustomer.printInfo()
   // Output:
   // Customer:
   // Name : John | Age: 25 | Location: FL
}

run()

Les inconvénients de ce modèle incluent une grande complexité de code puisqu’il n’y a pas de modèle standard défini pour ajouter de nouvelles fonctionnalités à l’aide de décorateurs. Vous risquez de vous retrouver avec un grand nombre de décorateurs non uniformes et/ou similaires à la fin du cycle de vie de votre développement logiciel.

Si vous ne faites pas attention lors de la conception des décorateurs, vous pourriez finir par concevoir certains décorateurs pour qu’ils soient logiquement dépendants d’autres. Si ce problème n’est pas résolu, la suppression ou la restructuration des décorateurs plus tard dans le cycle de vie peut avoir des conséquences désastreuses sur la stabilité de votre application.

10. Facade

Lors de la création de la plupart des applications du monde réel, la logique métier s’avère généralement assez complexe lorsque vous avez terminé. Vous pouvez vous retrouver avec plusieurs objets et méthodes impliqués dans l’exécution des opérations de base de votre application. Garder la trace de leurs initialisations, de leurs dépendances, de l’ordre correct d’exécution des méthodes, etc., peut être assez délicat et source d’erreurs si ce n’est pas fait correctement.

Le modèle de conception Facade vous aide à créer une abstraction entre l’environnement qui invoque les opérations susmentionnées et les objets et méthodes impliqués dans la réalisation de ces opérations. Cette abstraction abrite la logique d’initialisation des objets, le suivi de leurs dépendances et d’autres activités importantes. L’environnement d’appel n’a aucune information sur la façon dont une opération est exécutée. Vous pouvez librement mettre à jour la logique sans apporter de changement radical au client appelant.

Voici comment vous pouvez l’utiliser dans une application :

/**
* Let's say you're trying to build an online store. It will have multiple components and
* complex business logic. In the example below, you will find a tiny segment of an online
* store composed together using the Facade design pattern. The various manager and helper
* classes are defined first of all.
*/


function CartManager() {
   this.getItems = function() {
       // logic to return items
       return []
   }
  
   this.clearCart = function() {
       // logic to clear cart
   }
}

function InvoiceManager() {
   this.createInvoice = function(items) {
       // logic to create invoice
       return {}
   }

   this.notifyCustomerOfFailure = function(invoice) {
       // logic to notify customer
   }

   this.updateInvoicePaymentDetails = function(paymentResult) {
       // logic to update invoice after payment attempt
   }
}

function PaymentProcessor() {
   this.processPayment = function(invoice) {
       // logic to initiate and process payment
       return {}
   }
}

function WarehouseManager() {
   this.prepareForShipping = function(items, invoice) {
       // logic to prepare the items to be shipped
   }
}

// This is where facade comes in. You create an additional interface on top of your
// existing interfaces to define the business logic clearly. This interface exposes
// very simple, high-level methods for the calling environment.
function OnlineStore() {
   this.name = "Online Store"
  
   this.placeOrder = function() {
       let cartManager = new CartManager()
       let items = cartManager.getItems()

       let invoiceManager = new InvoiceManager()
       let invoice = invoiceManager.createInvoice(items)
      
       let paymentResult = new PaymentProcessor().processPayment(invoice)
       invoiceManager.updateInvoicePaymentDetails(paymentResult)

       if (paymentResult.status === 'success') {
           new WarehouseManager().prepareForShipping(items, invoice)
           cartManager.clearCart()
       } else {
           invoiceManager.notifyCustomerOfFailure(invoice)
       }
      
   }
}

// The calling environment is unaware of what goes on when somebody clicks a button to
// place the order. You can easily change the underlying business logic without breaking
// your calling environment.
function run() {
   let onlineStore = new OnlineStore()

   onlineStore.placeOrder()
}

L’inconvénient de l’utilisation du modèle Facade est qu’il ajoute une couche d’abstraction supplémentaire entre votre logique métier et le client, ce qui nécessite une maintenance supplémentaire. Le plus souvent, cela augmente la complexité globale de la base de code.

En plus de cela, la classe Facade devient une dépendance obligatoire pour le fonctionnement de votre application – ce qui signifie que toute erreur dans la classe Facade a un impact direct sur le fonctionnement de votre application.

11. Flyweight

Le modèle Flyweight vous aide à résoudre les problèmes impliquant des objets avec des composants répétitifs de manière efficace en termes de mémoire en vous aidant à réutiliser les composants communs de votre pool d’objets. Cela permet de réduire la charge de la mémoire et d’obtenir des temps d’exécution plus rapides.

Dans l’exemple ci-dessous, une grande phrase est stockée en mémoire à l’aide du modèle de conception Flyweight. Au lieu de stocker chaque caractère au fur et à mesure qu’il apparaît, le programme identifie l’ensemble des caractères distincts qui ont été utilisés pour écrire le paragraphe et leurs types (nombre ou alphabet) et construit des Flyweight réutilisables pour chaque caractère qui contient les détails du caractère et du type stockés.

Ensuite, le tableau principal stocke simplement une liste de références à ces poids volants dans l’ordre où ils apparaissent dans la phrase au lieu de stocker une instance de l’objet caractère à chaque fois qu’il apparaît.

Cela réduit de moitié la mémoire occupée par la phrase. Gardez à l’esprit qu’il s’agit d’une explication très basique de la manière dont les processeurs de texte stockent le texte.

// A simple Character class that stores the value, type, and position of a character
function Character(value, type, position) {
   this.value = value
   this.type = type
   this.position = position
}

// A Flyweight class that stores character value and type combinations
function CharacterFlyweight(value, type) {
   this.value = value
   this.type = type
}

// A factory to automatically create the flyweights that are not present in the list,
// and also generate a count of the total flyweights in the list
const CharacterFlyweightFactory = (function () {
   const flyweights = {}

   return {
       get: function (value, type) {
           if (flyweights[value + type] === undefined)
               flyweights[value + type] = new CharacterFlyweight(value, type)

           return flyweights[value + type]
       },
       count: function () {
           let count = 0;
           for (var f in flyweights) count++;
           return count;
       }
   }
})()

// An enhanced Character class that uses flyweights to store references
// to recurring value and type combinations
function CharacterWithFlyweight(value, type, position) {
   this.flyweight = CharacterFlyweightFactory.get(value, type)
   this.position = position
}

// A helper function to define the type of a character
// It identifies numbers as N and everything as A (for alphabets)
function getCharacterType(char) {
   switch (char) {
       case "0":
       case "1":
       case "2":
       case "3":
       case "4":
       case "5":
       case "6":
       case "7":
       case "8":
       case "9": return "N"
       default:
           return "A"

   }
}

// A list class to create an array of Characters from a given string
function CharactersList(str) {
   chars = []
   for (let i = 0; i < str.length; i++) {
       const char = str[i]
       chars.push(new Character(char, getCharacterType(char), i))
   }

   return chars
}

// A list class to create an array of CharacterWithFlyweights from a given string
function CharactersWithFlyweightsList(str) {
   chars = []
   for (let i = 0; i  " + charactersList.length)
   // Output: Character count -> 656

   // The number of flyweights created is only 31, since only 31 characters are used to write the
   // entire paragraph. This means that to store 656 characters, a total of
   // (31 * 2 + 656 * 1 = 718) memory blocks are used instead of (656 * 3 = 1968) which would have
   // used by the standard array.
   // (We have assumed each variable to take up one memory block for simplicity. This
   // may vary in real-life scenarios)
   console.log("Flyweights created -> " + CharacterFlyweightFactory.count())
   // Output: Flyweights created -> 31

}

run()

Comme vous l’avez peut-être déjà remarqué, le modèle Flyweight ajoute à la complexité de la conception de votre logiciel en n’étant pas particulièrement intuitif. Donc, si l’économie de mémoire n’est pas une préoccupation urgente pour votre application, la complexité supplémentaire de Flyweight peut faire plus de mal que de bien.

De plus, les Flyweight échangent la mémoire contre l’efficacité du traitement, donc si vous êtes à court de cycles CPU, Flyweight n’est pas une bonne solution pour vous.

12. Proxy

Le modèle Proxy vous permet de substituer un objet à un autre. En d’autres termes, les objets proxy peuvent prendre la place d’objets réels (dont ils sont un proxy) et contrôler l’accès à l’objet. Ces objets proxy peuvent être utilisés pour effectuer certaines actions avant ou après qu’une demande d’invocation soit transmise à l’objet réel.

Dans l’exemple ci-dessous, vous verrez comment l’accès à une instance de base de données est contrôlé par un proxy qui effectue quelques contrôles de validation de base sur les requêtes avant de les autoriser :

function DatabaseHandler() {
   const data = {}

   this.set = function (key, val) {
       data[key] = val;
   }
   this.get = function (key, val) {
       return data[key]
   }
   this.remove = function (key) {
       data[key] = null;
   }


}

function DatabaseProxy(databaseInstance) {

   this.set = function (key, val) {
       if (key === "") {
           console.log("Invalid input")
           return
       }

       if (val === undefined) {
           console.log("Setting value to undefined not allowed!")
           return
       }

       databaseInstance.set(key, val)
   }

   this.get = function (key) {
       if (databaseInstance.get(key) === null) {
           console.log("Element deleted")
       }

       if (databaseInstance.get(key) === undefined) {
           console.log("Element not created")
       }

       return databaseInstance.get(key)
   }

   this.remove = function (key) {
       if (databaseInstance.get(key) === undefined) {
           console.log("Element not added")
           return
       }

       if (databaseInstance.get(key) === null) {
           console.log("Element removed already")
           return
       }

       return databaseInstance.remove(key)
   }

}

function run() {
   let databaseInstance = new DatabaseHandler()

   databaseInstance.set("foo," "bar")
   databaseInstance.set("foo," undefined)
   console.log("#1: " + databaseInstance.get("foo"))
   // #1: undefined

   console.log("#2: " + databaseInstance.get("baz"))
   // #2: undefined

   databaseInstance.set("," "something")

   databaseInstance.remove("foo")
   console.log("#3: " + databaseInstance.get("foo"))
   // #3: null

   databaseInstance.remove("foo")
   databaseInstance.remove("baz")

   // Create a fresh database instance to try the same operations
   // using the proxy
   databaseInstance = new DatabaseHandler()
   let proxy = new DatabaseProxy(databaseInstance)

   proxy.set("foo," "bar")
   proxy.set("foo," undefined)
   // Proxy jumps in:
   // Output: Setting value to undefined not allowed!

   console.log("#1: " + proxy.get("foo"))
   // Original value is retained:
   // Output: #1: bar

   console.log("#2: " + proxy.get("baz"))
   // Proxy jumps in again
   // Output:
   // Element not created
   // #2: undefined


   proxy.set("," "something")
   // Proxy jumps in again
   // Output: Invalid input

   proxy.remove("foo")

   console.log("#3: " + proxy.get("foo"))
   // Proxy jumps in again
   // Output:
   // Element deleted
   // #3: null

   proxy.remove("foo")
   // Proxy output: Element removed already
   proxy.remove("baz")
   // Proxy output: Element not added

}

run()

Ce modèle de conception est couramment utilisé dans l’industrie et permet de mettre en œuvre facilement les opérations de pré-exécution et de post-exécution. Cependant, comme tout autre modèle de conception, il ajoute de la complexité à votre base de code, alors essayez de ne pas l’utiliser si vous n’en avez pas vraiment besoin.

Vous devez également garder à l’esprit que, puisqu’un objet supplémentaire est impliqué dans les appels à votre objet réel, il peut y avoir une certaine latence due aux opérations de traitement supplémentaires. L’optimisation des performances de votre objet principal implique maintenant aussi l’optimisation des méthodes de votre proxy pour les performances.

Comportemental

Les modèles de conception comportementaux vous aident à résoudre les problèmes liés à la manière dont les objets interagissent les uns avec les autres. Cela peut impliquer le partage ou le passage de la responsabilité/du contrôle entre les objets pour effectuer des opérations d’ensemble. Il peut également s’agir de transmettre/partager des données entre plusieurs objets de la manière la plus efficace possible.

13. Chain of Responsability

Le modèle Chain of Responsability est l’un des modèles de conception comportementale les plus simples. Il est utile lorsque vous concevez une logique pour des opérations qui peuvent être traitées par plusieurs gestionnaires.

À l’instar de l’escalade des problèmes dans les équipes de support, le contrôle passe par une chaîne de gestionnaires, et le gestionnaire responsable de l’action termine l’opération. Ce modèle de conception est souvent utilisé dans la conception d’interfaces utilisateur, où plusieurs couches de composants peuvent gérer un événement d’entrée utilisateur, tel qu’un toucher ou un glissement.

Vous verrez ci-dessous un exemple d’escalade de plainte utilisant le modèle Chaine of Responsability. La plainte sera traitée par les gestionnaires en fonction de sa gravité :

// Complaint class that stores title and severity of a complaint
// Higher value of severity indicates a more severe complaint
function Complaint (title, severity) {
    this.title = title
    this.severity = severity
}

// Base level handler that receives all complaints
function Representative () {
    // If this handler can not handle the complaint, it will be forwarded to the next level
    this.nextLevel = new Management()

    this.handleComplaint = function (complaint) {
        if (complaint.severity === 0)
            console.log("Representative resolved the following complaint: " + complaint.title)
        else
            this.nextLevel.handleComplaint(complaint)
    }
}

// Second level handler to handle complaints of severity 1
function Management() {
    // If this handler can not handle the complaint, it will be forwarded to the next level
    this.nextLevel = new Leadership()

    this.handleComplaint = function (complaint) {
        if (complaint.severity === 1)
            console.log("Management resolved the following complaint: " + complaint.title)
        else
            this.nextLevel.handleComplaint(complaint)
    }
}

// Highest level handler that handles all complaints unhandled so far
function Leadership() {
    this.handleComplaint = function (complaint) {
        console.log("Leadership resolved the following complaint: " + complaint.title)
    }
}

function run() {
    // Create an instance of the base level handler
    let customerSupport = new Representative()

    // Create multiple complaints of varying severity and pass them to the base handler

    let complaint1 = new Complaint("Submit button doesn't work," 0)
    customerSupport.handleComplaint(complaint1)
    // Output: Representative resolved the following complaint: Submit button doesn't work

    let complaint2 = new Complaint("Payment failed," 1)
    customerSupport.handleComplaint(complaint2)
    // Output: Management resolved the following complaint: Payment failed

    let complaint3 = new Complaint("Employee misdemeanour," 2)
    customerSupport.handleComplaint(complaint3)
    // Output: Leadership resolved the following complaint: Employee misdemeanour
}

run()

Le problème évident de ce modèle est qu’il est linéaire. Il peut donc y avoir une certaine latence dans le traitement d’une opération lorsqu’un grand nombre de gestionnaires sont enchaînés les uns aux autres.

Garder la trace de tous les gestionnaires peut être un autre point douloureux, car cela peut devenir assez désordonné après un certain nombre de gestionnaires. Le débogage est encore un autre cauchemar, car chaque requête peut se terminer sur un gestionnaire différent, ce qui rend difficile la normalisation du processus de journalisation et de débogage.

14. Iterator

Le modèle Iterator est assez simple et est très couramment utilisé dans presque tous les langages modernes orientés objet. Si vous devez parcourir une liste d’objets qui ne sont pas tous du même type, les méthodes d’itération normales, telles que les boucles for, peuvent devenir très compliquées, surtout si elles contiennent une logique d’entreprise.

Le modèle Iterator peut vous aider à isoler la logique d’itération et de traitement de vos listes de la logique métier principale.

Voici comment vous pouvez l’utiliser sur une liste plutôt basique avec plusieurs types d’éléments :

// Iterator for a complex list with custom methods
function Iterator(list) {
   this.list = list
   this.index = 0

   // Fetch the current element
   this.current = function() {
       return this.list[this.index]
   }

   // Fetch the next element in the list
   this.next = function() {
       return this.list[this.index++]
   }

   // Check if there is another element in the list
   this.hasNext = function() {
       return this.index < this.list.length
   }

   // Reset the index to point to the initial element
   this.resetIndex = function() {
       this.index = 0
   }

   // Run a forEach loop over the list
   this.forEach = function(callback) {
       for (let element = this.next(); this.index <= this.list.length; element = this.next()) {
           callback(element)
       }
   }
}

function run() {
   // A complex list with elements of multiple data types
   let list = ["Lorem ipsum," 9, ["lorem ipsum dolor," true], false]

   // Create an instance of the iterator and pass it the list
   let iterator = new Iterator(list)

   // Log the first element
   console.log(iterator.current())
   // Output: Lorem ipsum

   // Print all elements of the list using the iterator's methods
   while (iterator.hasNext()) {
       console.log(iterator.next())
       /**
        * Output:
        * Lorem ipsum
        * 9
        * [ 'lorem ipsum dolor', true ]
        * false
        */
   }

   // Reset the iterator's index to the first element
   iterator.resetIndex()

   // Use the custom iterator to pass an effect that will run for each element of the list
   iterator.forEach(function (element) {
       console.log(element)
   })
   /**
    * Output:
    * Lorem ipsum
    * 9
    * [ 'lorem ipsum dolor', true ]
    * false
    */
}

run()

Il va sans dire que ce mdèle peut être inutilement complexe pour les listes sans plusieurs types d’éléments. De plus, s’il y a trop de types d’éléments dans une liste, celle-ci peut également devenir difficile à gérer.

La clé est d’identifier si vous avez vraiment besoin d’un itérateur en fonction de votre liste et de ses possibilités de modifications futures. De plus, le modèle Iterator n’est utile que dans les listes, et les listes peuvent parfois vous limiter à leur mode d’accès linéaire. D’autres structures de données peuvent parfois vous offrir des avantages plus importants en termes de performances.

15. Mediator

La conception de votre application peut parfois vous obliger à jouer avec un grand nombre d’objets distincts qui abritent divers types de logique commerciale et dépendent souvent les uns des autres. La gestion des dépendances peut parfois s’avérer délicate car vous devez garder la trace de la manière dont ces objets échangent des données et du contrôle entre eux.

Le modèle de conception Mediator a pour but de vous aider à résoudre ce problème en isolant la logique d’interaction de ces objets dans un objet distinct en soi.

Cet objet distinct est connu sous le nom de Mediator, et il est responsable de l’exécution du travail effectué par vos classes de niveau inférieur. Votre client ou l’environnement appelant interagira également avec Mediator au lieu des classes de niveau inférieur.

Voici un exemple du modèle de conception Mediator en action :

// Writer class that receives an assignment, writes it in 2 seconds, and marks it as finished
function Writer(name, manager) {
    
    // Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
    this.manager = manager
    this.name = name
    this.busy = false

    this.startWriting = function (assignment) {
        console.log(this.name + " started writing "" + assignment + """)
        this.assignment = assignment
        this.busy = true

        // 2 s timer to replicate manual action
        setTimeout(() => { this.finishWriting() }, 2000)
    }

    this.finishWriting = function () {
        if (this.busy === true) {
            console.log(this.name + " finished writing "" + this.assignment + """)
            this.busy = false
            return this.manager.notifyWritingComplete(this.assignment)
        } else {
            console.log(this.name + " is not writing any article")
        }
    }
}

// Editor class that receives an assignment, edits it in 3 seconds, and marks it as finished
function Editor(name, manager) {
    
    // Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
    this.manager = manager
    this.name = name
    this.busy = false

    this.startEditing = function (assignment) {
        console.log(this.name + " started editing "" + assignment + """)
        this.assignment = assignment
        this.busy = true

        // 3 s timer to replicate manual action
        setTimeout(() => { this.finishEditing() }, 3000)
    }

    this.finishEditing = function () {
        if (this.busy === true) {
            console.log(this.name + " finished editing "" + this.assignment + """)
            this.manager.notifyEditingComplete(this.assignment)
            this.busy = false
        } else {
            console.log(this.name + " is not editing any article")
        }
    }
}

// The mediator class
function Manager() {
    // Store arrays of workers
    this.editors = []
    this.writers = []

    this.setEditors = function (editors) {
        this.editors = editors
    }
    this.setWriters = function (writers) {
        this.writers = writers
    }

    // Manager receives new assignments via this method
    this.notifyNewAssignment = function (assignment) {
        let availableWriter = this.writers.find(function (writer) {
            return writer.busy === false
        })
        availableWriter.startWriting(assignment)
        return availableWriter
    }

    // Writers call this method to notify they're done writing
    this.notifyWritingComplete = function (assignment) {
        let availableEditor = this.editors.find(function (editor) {
            return editor.busy === false
        })
        availableEditor.startEditing(assignment)
        return availableEditor
    }

    // Editors call this method to notify they're done editing
    this.notifyEditingComplete = function (assignment) {
        console.log(""" + assignment + "" is ready to publish")
    }

}

function run() {
    // Create a manager
    let manager = new Manager()

    // Create workers
    let editors = [
        new Editor("Ed," manager),
        new Editor("Phil," manager),
    ]

    let writers = [
        new Writer("Michael," manager),
        new Writer("Rick," manager),
    ]

    // Attach workers to manager
    manager.setEditors(editors)
    manager.setWriters(writers)

    // Send two assignments to manager
    manager.notifyNewAssignment("var vs let in JavaScript")
    manager.notifyNewAssignment("JS promises")

    /**
     * Output:
     * Michael started writing "var vs let in JavaScript"
     * Rick started writing "JS promises"
     * 
     * After 2s, output:
     * Michael finished writing "var vs let in JavaScript"
     * Ed started editing "var vs let in JavaScript"
     * Rick finished writing "JS promises"
     * Phil started editing "JS promises"
     *
     * After 3s, output:
     * Ed finished editing "var vs let in JavaScript"
     * "var vs let in JavaScript" is ready to publish
     * Phil finished editing "JS promises"
     * "JS promises" is ready to publish
     */

}

run()

Bien que Mediator offre à la conception de votre application un découplage et une grande flexibilité, en fin de compte, il s’agit d’une autre classe que vous devez maintenir. Vous devez évaluer si votre conception peut vraiment bénéficier d’un Mediator avant d’en écrire un afin de ne pas finir par ajouter une complexité inutile à votre base de code.

Il est également important de garder à l’esprit que même si la classe Mediator ne contient pas de logique commerciale directe, elle contient tout de même beaucoup de code crucial pour le fonctionnement de votre application et peut donc rapidement devenir assez complexe.

16. Memento

Le versionnement des objets est un autre problème courant auquel vous serez confronté lors du développement d’applications. Dans de nombreux cas d’utilisation, il est nécessaire de conserver l’historique d’un objet, de permettre des retours en arrière faciles et parfois même de revenir en arrière. L’écriture de la logique pour de telles applications peut être difficile.

Le modèle de conception Memento est destiné à résoudre facilement ce problème.

Memento est considéré comme un instantané d’un objet à un moment donné. Le modèle de conception Memento utilise ces mémentos pour préserver des instantanés de l’objet au fur et à mesure qu’il est modifié au fil du temps. Lorsque vous avez besoin de revenir à une ancienne version, il vous suffit de récupérer le memento correspondant.

Voici comment vous pouvez l’implémenter dans une application de traitement de texte :

// The memento class that can hold one snapshot of the Originator class - document
function Text(contents) {
    // Contents of the document
    this.contents = contents

    // Accessor function for contents
    this.getContents = function () {
        return this.contents
    }

    // Helper function to calculate word count for the current document
    this.getWordCount = function () {
        return this.contents.length
    }
}

// The originator class that holds the latest version of the document
function Document(contents) {
    // Holder for the memento, i.e., the text of the document
    this.text = new Text(contents)

    // Function to save new contents as a memento
    this.save = function (contents) {
        this.text = new Text(contents)
        return this.text
    }

    // Function to revert to an older version of the text using a memento
    this.restore = function (text) {
        this.text = new Text(text.getContents())
    }

    // Helper function to get the current memento
    this.getText = function () {
        return this.text
    }

    // Helper function to get the word count of the current document
    this.getWordCount = function () {
        return this.text.getWordCount()
    }
}

// The caretaker class that providers helper functions to modify the document
function DocumentManager(document) {
    // Holder for the originator, i.e., the document
    this.document = document

    // Array to maintain a list of mementos
    this.history = []

    // Add the initial state of the document as the first version of the document
    this.history.push(document.getText())

    // Helper function to get the current contents of the documents
    this.getContents = function () {
        return this.document.getText().getContents()
    }

    // Helper function to get the total number of versions available for the document
    this.getVersionCount = function () {
        return this.history.length
    }

    // Helper function to get the complete history of the document
    this.getHistory = function () {
        return this.history.map(function (element) {
            return element.getContents()
        })

    }

    // Function to overwrite the contents of the document
    this.overwrite = function (contents) {
        let newVersion = this.document.save(contents)
        this.history.push(newVersion)
    }

    // Function to append new content to the existing contents of the document
    this.append = function (contents) {
        let currentVersion = this.history[this.history.length - 1]
        let newVersion
        if (currentVersion === undefined)
            newVersion = this.document.save(contents)
        else
            newVersion = this.document.save(currentVersion.getContents() + contents)
        this.history.push(newVersion)
    }

    // Function to delete all the contents of the document
    this.delete = function () {
        this.history.push(this.document.save(""))
    }

    // Function to get a particular version of the document
    this.getVersion = function (versionNumber) {
        return this.history[versionNumber - 1]
    }

    // Function to undo the last change
    this.undo = function () {
        let previousVersion = this.history[this.history.length - 2]
        this.document.restore(previousVersion)
        this.history.push(previousVersion)
    }

    // Function to revert the document to a previous version
    this.revertToVersion = function (version) {
        let previousVersion = this.history[version - 1]
        this.document.restore(previousVersion)
        this.history.push(previousVersion)
    }

    // Helper function to get the total word count of the document
    this.getWordCount = function () {
        return this.document.getWordCount()
    }

}

function run() {
    // Create a document
    let blogPost = new Document("")

    // Create a caretaker for the document
    let blogPostManager = new DocumentManager(blogPost)

    // Change #1: Add some text
    blogPostManager.append("Hello World!")
    console.log(blogPostManager.getContents())
    // Output: Hello World!

    // Change #2: Add some more text
    blogPostManager.append(" This is the second entry in the document")
    console.log(blogPostManager.getContents())
    // Output: Hello World! This is the second entry in the document

    // Change #3: Overwrite the document with some new text
    blogPostManager.overwrite("This entry overwrites everything in the document")
    console.log(blogPostManager.getContents())
    // Output: This entry overwrites everything in the document

    // Change #4: Delete the contents of the document
    blogPostManager.delete()
    console.log(blogPostManager.getContents())
    // Empty output

    // Get an old version of the document
    console.log(blogPostManager.getVersion(2).getContents())
    // Output: Hello World!

    // Change #5: Go back to an old version of the document
    blogPostManager.revertToVersion(3)
    console.log(blogPostManager.getContents())
    // Output: Hello World! This is the second entry in the document

    // Get the word count of the current document
    console.log(blogPostManager.getWordCount())
    // Output: 53

    // Change #6: Undo the last change
    blogPostManager.undo()
    console.log(blogPostManager.getContents())
    // Empty output

    // Get the total number of versions for the document
    console.log(blogPostManager.getVersionCount())
    // Output: 7

    // Get the complete history of the document
    console.log(blogPostManager.getHistory())
    /**
     * Output:
     * [
     *   '',
     *   'Hello World!',
     *   'Hello World! This is the second entry in the document',
     *   'This entry overwrites everything in the document',
     *   '',
     *   'Hello World! This is the second entry in the document',
     *   ''
     * ]
     */
}

run()

Bien que le modèle de conception Memento soit une excellente solution pour gérer l’historique d’un objet, il peut être très gourmand en ressources. Comme chaque memento est presque une copie de l’objet, il peut faire gonfler la mémoire de votre application très rapidement s’il n’est pas utilisé avec modération.

Avec un grand nombre d’objets, la gestion de leur cycle de vie peut également être une tâche assez fastidieuse. En plus de tout cela, les classes Originator et Caretaker sont généralement très étroitement couplées, ce qui ajoute à la complexité de votre base de code.

17. Observer

Le modèle Observer fournit une solution alternative au problème de l’interaction entre plusieurs objets (vu précédemment dans le modèle Mediator).

Au lieu de permettre à chaque objet de communiquer entre eux par le biais d’un médiateur désigné, le modèle Observer leur permet de s’observer mutuellement. Les objets sont conçus pour émettre des évènements lorsqu’ils tentent d’envoyer des données ou un contrôle, et les autres objets qui « écoutent » ces évènements peuvent ensuite les recevoir et interagir en fonction de leur contenu.

Voici une démonstration simple de l’envoi de bulletins d’information à plusieurs personnes par le biais du modèle Observer :

// The newsletter class that can send out posts to its subscribers
function Newsletter() {
   // Maintain a list of subscribers
   this.subscribers = []

   // Subscribe a reader by adding them to the subscribers' list
   this.subscribe = function(subscriber) {
       this.subscribers.push(subscriber)
   }

   // Unsubscribe a reader by removing them from the subscribers' list
   this.unsubscribe = function(subscriber) {
       this.subscribers = this.subscribers.filter(
           function (element) {
               if (element !== subscriber) return element
           }
       )
   }

   // Publish a post by calling the receive function of all subscribers
   this.publish = function(post) {
       this.subscribers.forEach(function(element) {
           element.receiveNewsletter(post)
       })
   }
}

// The reader class that can subscribe to and receive updates from newsletters
function Reader(name) {
   this.name = name

   this.receiveNewsletter = function(post) {
       console.log("Newsletter received by " + name + "!: " + post)
   }

}

function run() {
   // Create two readers
   let rick = new Reader("ed")
   let morty = new Reader("morty")

   // Create your newsletter
   let newsletter = new Newsletter()

   // Subscribe a reader to the newsletter
   newsletter.subscribe(rick)

   // Publish the first post
   newsletter.publish("This is the first of the many posts in this newsletter")
   /**
    * Output:
    * Newsletter received by ed!: This is the first of the many posts in this newsletter
    */

   // Subscribe another reader to the newsletter
   newsletter.subscribe(morty)

   // Publish the second post
   newsletter.publish("This is the second of the many posts in this newsletter")
   /**
    * Output:
    * Newsletter received by ed!: This is the second of the many posts in this newsletter
    * Newsletter received by morty!: This is the second of the many posts in this newsletter
    */

   // Unsubscribe the first reader
   newsletter.unsubscribe(rick)

   // Publish the third post
   newsletter.publish("This is the third of the many posts in this newsletter")
   /**
    * Output:
    * Newsletter received by morty!: This is the third of the many posts in this newsletter
    */

}

run()

Bien que le modèle Observer soit un moyen astucieux de faire circuler le contrôle et les données, il est mieux adapté aux situations où il y a un grand nombre d’émetteurs et de récepteurs qui interagissent entre eux via un nombre limité de connexions. Si les objets devaient tous établir des connexions un à un, vous perdriez l’avantage que vous obtenez en publiant et en vous abonnant à des évènements puisqu’il n’y aura toujours qu’un seul abonné pour chaque éditeur (alors qu’il aurait été mieux géré par une ligne de communication directe entre eux).

En outre, le modèle de conception Observer peut entraîner des problèmes de performance si les événements d’abonnement ne sont pas traités correctement. Si un objet continue à s’abonner à un autre objet même s’il n’en a pas besoin, il ne sera pas éligible à la collecte et augmentera la consommation de mémoire de l’application.

18. State

Le modèle de conception State est l’un des modèles de conception les plus utilisés dans le secteur du développement logiciel. Les frameworks JavaScript populaires comme React et Angular s’appuient fortement sur le modèle State pour gérer les données et le comportement de l’application en fonction de ces données.

En d’autres termes, le modèle de conception State est utile dans les situations où vous pouvez définir les états définitifs d’une entité (qui peut être un composant, une page, une application ou une machine), et où l’entité a une réaction prédéfinie au changement d’état.

Disons que vous essayez de construire un processus de demande de prêt. Chaque étape du processus de demande peut être définie comme un état.

Alors que le client voit généralement une petite liste d’états simplifiés de sa demande (en attente, en révision, acceptée et rejetée), d’autres étapes peuvent être impliquées en interne. À chacune de ces étapes, la demande sera attribuée à une personne distincte et peut avoir des exigences uniques.

Le système est conçu de telle sorte qu’à la fin du traitement dans un état, l’état est mis à jour et remplacé par le suivant, et la prochaine série d’étapes pertinentes est lancée.

Voici comment vous pouvez construire un système de gestion des tâches en utilisant le modèle de conception State :

// Create titles for all states of a task
const STATE_TODO = "TODO"
const STATE_IN_PROGRESS = "IN_PROGRESS"
const STATE_READY_FOR_REVIEW = "READY_FOR_REVIEW"
const STATE_DONE = "DONE"

// Create the task class with a title, assignee, and duration of the task
function Task(title, assignee) {
    this.title = title
    this.assignee = assignee

    // Helper function to update the assignee of the task
    this.setAssignee = function (assignee) {
        this.assignee = assignee
    }

    // Function to update the state of the task
    this.updateState = function (state) {

        switch (state) {
            case STATE_TODO:
                this.state = new TODO(this)
                break
            case STATE_IN_PROGRESS:
                this.state = new IN_PROGRESS(this)
                break
            case STATE_READY_FOR_REVIEW:
                this.state = new READY_FOR_REVIEW(this)
                break
            case STATE_DONE:
                this.state = new DONE(this)
                break
            default:
                return
        }
        // Invoke the callback function for the new state after it is set
        this.state.onStateSet()
    }

    // Set the initial state of the task as TODO
    this.updateState(STATE_TODO)
}

// TODO state
function TODO(task) {

    this.onStateSet = function () {
        console.log(task.assignee + " notified about new task "" + task.title + """)
    }
}

// IN_PROGRESS state
function IN_PROGRESS(task) {

    this.onStateSet = function () {
        console.log(task.assignee + " started working on the task "" + task.title + """)
    }
}

// READY_FOR_REVIEW state that updates the assignee of the task to be the manager of the developer
// for the review
function READY_FOR_REVIEW(task) {
    this.getAssignee = function () {
        return "Manager 1"
    }

    this.onStateSet = function () {
        task.setAssignee(this.getAssignee())
        console.log(task.assignee + " notified about completed task "" + task.title + """)
    }
}

// DONE state that removes the assignee of the task since it is now completed
function DONE(task) {
    this.getAssignee = function () {
        return ""
    }

    this.onStateSet = function () {
        task.setAssignee(this.getAssignee())
        console.log("Task "" + task.title + "" completed")
    }
}

function run() {
    // Create a task
    let task1 = new Task("Create a login page," "Developer 1")
    // Output: Developer 1 notified about new task "Create a login page"

    // Set it to IN_PROGRESS
    task1.updateState(STATE_IN_PROGRESS)
    // Output: Developer 1 started working on the task "Create a login page"

    // Create another task
    let task2 = new Task("Create an auth server," "Developer 2")
    // Output: Developer 2 notified about new task "Create an auth server"


    // Set it to IN_PROGRESS as well
    task2.updateState(STATE_IN_PROGRESS)
    // Output: Developer 2 started working on the task "Create an auth server"

    // Update the states of the tasks until they are done
    task2.updateState(STATE_READY_FOR_REVIEW)
    // Output: Manager 1 notified about completed task "Create an auth server"
    task1.updateState(STATE_READY_FOR_REVIEW)
    // Output: Manager 1 notified about completed task "Create a login page"


    task1.updateState(STATE_DONE)
    // Output: Task "Create a login page" completed
    task2.updateState(STATE_DONE)
    // Output: Task "Create an auth server" completed

}

run()

Bien que le modèle State fasse un excellent travail de ségrégation des étapes d’un processus, il peut devenir extrêmement difficile à maintenir dans les grandes applications qui ont plusieurs états.

De plus, si la conception de votre processus permet plus qu’un déplacement linéaire à travers tous les états, vous devrez écrire et maintenir plus de code, puisque chaque transition d’état doit être traitée séparément.

19. Strategy

Également connu sous le nom de modèle Policy, le modèle Strategy vise à vous aider à encapsuler et à échanger librement des classes à l’aide d’une interface commune. Cela permet de maintenir un couplage lâche entre le client et les classes et vous permet d’ajouter autant d’implémentations que vous le souhaitez.

Le modèle Strategy est connu pour son aide considérable dans les situations où la même opération est nécessaire en utilisant différentes méthodes/algorithmes, ou lorsque des blocs de commutation massifs doivent être remplacés par un code plus convivial.

Voici un exemple du modèle Strategy :

// The strategy class that can encapsulate all hosting providers
function HostingProvider() {
   // store the provider
   this.provider = ""

   // set the provider
   this.setProvider = function(provider) {
       this.provider = provider
   }

   // set the website configuration for which each hosting provider would calculate costs
   this.setConfiguration = function(configuration) {
       this.configuration = configuration
   }

   // the generic estimate method that calls the provider's unique methods to calculate the costs
   this.estimateMonthlyCost = function() {
       return this.provider.estimateMonthlyCost(this.configuration)
   }
}

// Foo Hosting charges for each second and KB of hosting usage
function FooHosting (){
   this.name = "FooHosting"
   this.rate = 0.0000027

   this.estimateMonthlyCost = function(configuration){
       return configuration.duration * configuration.workloadSize * this.rate
   }
}

// Bar Hosting charges per minute instead of seconds
function BarHosting (){
   this.name = "BarHosting"
   this.rate = 0.00018

   this.estimateMonthlyCost = function(configuration){
       return configuration.duration / 60 * configuration.workloadSize * this.rate
   }
}

// Baz Hosting assumes the average workload to be of 10 MB in size
function BazHosting (){
   this.name = "BazHosting"
   this.rate = 0.032

   this.estimateMonthlyCost = function(configuration){
       return configuration.duration * this.rate
   }
}

function run() {

   // Create a website configuration for a website that is up for 24 hours and takes 10 MB of hosting space
   let workloadConfiguration = {
       duration: 84700,
       workloadSize: 10240
   }

   // Create the hosting provider instances
   let fooHosting = new FooHosting()
   let barHosting = new BarHosting()
   let bazHosting = new BazHosting()

   // Create the instance of the strategy class
   let hostingProvider = new HostingProvider()

   // Set the configuration against which the rates have to be calculated
   hostingProvider.setConfiguration(workloadConfiguration)

   // Set each provider one by one and print the rates
   hostingProvider.setProvider(fooHosting)
   console.log("FooHosting cost: " + hostingProvider.estimateMonthlyCost())
   // Output: FooHosting cost: 2341.7856

   hostingProvider.setProvider(barHosting)
   console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
   // Output: BarHosting cost: 2601.9840

   hostingProvider.setProvider(bazHosting)
   console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
   // Output: BarHosting cost: 2710.4000

}

run()

Le modèle Strategy est excellent lorsqu’il s’agit d’introduire de nouvelles variations d’une entité sans trop changer les clients. Cependant, il peut sembler exagéré si vous n’avez qu’une poignée de variations à mettre en œuvre.

De plus, l’encapsulation supprime les détails les plus fins concernant la logique interne de chaque variante, de sorte que votre client ne sait pas comment une variante va se comporter.

20. Visitor

Le modèle Visitor a pour but de vous aider à rendre votre code extensible.

L’idée est de fournir une méthode dans la classe qui permet aux objets d’autres classes d’apporter facilement des modifications aux objets de la classe actuelle. Les autres objets visitent l’objet courant (également appelé objet place), ou la classe courante accepte les objets visiteurs, et l’objet lieu gère la visite de chaque objet externe de manière appropriée.

Voici comment vous pouvez l’utiliser :

// Visitor class that defines the methods to be called when visiting each place
function Reader(name, cash) {
    this.name = name
    this.cash = cash

    // The visit methods can access the place object and invoke available functions
    this.visitBookstore = function(bookstore) {
        console.log(this.name + " visited the bookstore and bought a book")
        bookstore.purchaseBook(this)
    }

    this.visitLibrary = function() {
        console.log(this.name + " visited the library and read a book")
    }

    // Helper function to demonstrate a transaction
    this.pay = function(amount) {
        this.cash -= amount
    }
}

// Place class for a library
function Library () {
    this.accept = function(reader) {
        reader.visitLibrary()
    }
}

// Place class for a bookstore that allows purchasing book
function Bookstore () {
    this.accept = function(reader) {
        reader.visitBookstore(this)
    }

    this.purchaseBook = function (visitor) {
        console.log(visitor.name + " bought a book")
        visitor.pay(8)
    }
}


function run() {
    // Create a reader (the visitor)
    let reader = new Reader("Rick," 30)

    // Create the places
    let booksInc = new Bookstore()
    let publicLibrary = new Library()

    // The reader visits the library
    publicLibrary.accept(reader)
    // Output: Rick visited the library and read a book
    console.log(reader.name + " has $" + reader.cash)
    // Output: Rick has $30

    // The reader visits the bookstore
    booksInc.accept(reader)
    // Output: Rick visited the bookstore and bought a book
    console.log(reader.name + " has $" + reader.cash)
    // Output: Rick has $22
}

run()

Le seul défaut de cette conception est que chaque classe de visiteur doit être mise à jour chaque fois qu’un nouvelle place est ajoutée ou modifiée. Dans les cas où plusieurs visiteurs et objets lieu existent ensemble, cela peut être difficile à maintenir.

À part cela, cette méthode fonctionne très bien pour améliorer la fonctionnalité des classes de manière dynamique.

Meilleures pratiques pour la mise en œuvre des Design Patterns

Maintenant que vous avez vu les modèles de conception les plus courants en JavaScript, voici quelques conseils que vous devez garder à l’esprit lors de leur mise en œuvre.

Prenez soin de comprendre si un modèle convient à la solution

Ce conseil doit être appliqué avant d’implémenter un modèle de conception dans votre code source. Bien qu’il puisse sembler qu’un modèle de conception soit la fin de tous vos soucis, prenez un moment pour analyser de manière critique si cela est vrai.

Il existe de nombreux modèles qui résolvent le même problème mais adoptent des approches différentes et ont des conséquences différentes. Votre critère de sélection d’un modèle de conception ne doit donc pas se limiter à savoir s’il résout votre problème ou non – il doit également porter sur la manière dont il résout votre problème et sur l’existence d’un autre modèle pouvant présenter une solution plus efficace.

Comprendre les coûts de mise en œuvre d’un modèle avant de commencer

Bien que les modèles de conception semblent être la meilleure solution à tous les problèmes d’ingénierie, vous ne devriez pas vous lancer tout de suite dans leur mise en œuvre dans votre code source.

Tout en jugeant des conséquences de la mise en œuvre d’une solution, vous devez également prendre en considération votre propre situation. Disposez-vous d’une grande équipe de développeurs de logiciels qui sont parfaitement capables de comprendre et de maintenir les modèles de conception ? Ou êtes-vous un fondateur en phase de démarrage avec une équipe de développement minimale qui cherche à publier rapidement un MVP de votre produit ? Si vous répondez oui à la dernière question, les modèles de conception ne sont peut-être pas le mode de développement le plus optimal pour vous.

Les modèles de conception n’entraînent pas une forte réutilisation du code, à moins qu’ils ne soient planifiés à un stade très précoce de la conception de l’application. L’utilisation aléatoire de modèles de conception à différents stades peut conduire à une architecture d’application inutilement complexe que vous devrez passer des semaines à simplifier.

L’efficacité d’un modèle de conception ne peut être jugée par aucune forme de test. C’est l’expérience et l’introspection de votre équipe qui vous permettront de savoir s’ils fonctionnent. Si vous avez le temps et les ressources à consacrer à ces aspects, ce n’est qu’à ce moment-là que les modèles de conception résoudront vraiment vos problèmes.

Ne transformez pas chaque solution en un modèle

Une autre règle à garder à l’esprit est de s’abstenir d’essayer de transformer chaque petite paire problème-solution en un modèle de conception et de l’utiliser partout où vous voyez de la place pour lui.

S’il est bon d’identifier des solutions standard et de les garder à l’esprit lorsque vous rencontrez des problèmes similaires, il y a de fortes chances que le nouveau problème rencontré ne corresponde pas exactement à la même description qu’un ancien problème. Dans ce cas, vous pourriez finir par mettre en œuvre une solution sous-optimale et gaspiller des ressources.

Les modèles de conception s’imposent aujourd’hui comme des exemples de paires problème-solution parce qu’ils ont été testés par des centaines et des milliers de programmeurs au fil du temps et ont été généralisés autant que possible. Si vous essayez de reproduire cet effort en vous contentant d’examiner un tas de problèmes et de solutions et de les qualifier de similaires, vous pourriez finir par causer beaucoup plus de dégâts à votre code que vous ne l’auriez imaginé.

Quand utiliser les Design Patterns ?

Pour résumer, voici quelques indices auxquels vous devez faire attention pour utiliser les modèles de conception. Ils ne s’appliquent pas tous au développement de chaque application, mais ils devraient vous donner une bonne idée de ce à quoi vous devez faire attention lorsque vous envisagez d’utiliser des modèles de conception :

  • Vous disposez d’une solide équipe interne de développeurs qui comprend bien les modèles de conception.
  • Vous suivez un modèle SDLC qui laisse la place à des discussions approfondies sur l’architecture de votre application, et les modèles de conception ont été évoqués lors de ces discussions.
  • Le même ensemble de problèmes est apparu plusieurs fois dans vos discussions sur la conception, et vous connaissez le modèle de conception qui conviendra dans ce cas.
  • Vous avez essayé de résoudre une variante plus petite de votre problème indépendamment du modèle de conception.
  • Avec le modèle de conception en place, votre code ne semble pas excessivement complexe.

Si un modèle de conception résout votre problème et vous aide à écrire un code simple, réutilisable, modulaire, faiblement couplé et sans « odeur de code », c’est peut-être la bonne voie à suivre.

Un autre bon conseil à garder à l’esprit est d’éviter de tout ramener aux modèles de conception. Les modèles de conception sont destinés à vous aider à résoudre des problèmes. Ils ne sont pas des lois à respecter ou des règles à suivre à la lettre. Les règles et lois ultimes sont toujours les mêmes : gardez votre code propre, simple, lisible et évolutif. Si un modèle de conception vous aide à y parvenir tout en résolvant votre problème, vous devriez pouvoir l’utiliser.

Résumé

Les modèles de conception JavaScript sont une merveilleuse façon d’aborder les problèmes auxquels de nombreux programmeurs ont été confrontés au fil du temps. Ils présentent des solutions éprouvées qui s’efforcent de garder votre base de code propre et faiblement couplée.

Aujourd’hui, il existe des centaines de modèles de conception disponibles qui résoudront presque tous les problèmes que vous rencontrez lors de la création d’applications. Cependant, tous les modèles de conception ne résoudront pas vraiment votre problème à chaque fois.

Comme toute autre convention de programmation, les modèles de conception sont destinés à être pris comme des suggestions pour résoudre les problèmes. Ce ne sont pas des lois à suivre en permanence, et si vous les traitez comme des lois, vous pourriez finir par causer beaucoup de dégâts à vos applications.

Une fois votre application terminée, vous aurez besoin d’un endroit pour l’héberger – et les solutions d’hébergement d’applications de Kinsta sont en tête des plus rapides, des plus fiables et des plus sûres. Il vous suffit de vous connecter à votre compte MyKinsta (le tableau de bord personnalisé de Kinsta), de vous connecter à votre dépôt GitHub, et de lancer ! De plus, vous n’êtes facturé que pour les ressources que votre application utilise.

Quels sont les modèles de conception que vous utilisez régulièrement dans votre travail de programmation de logiciels ? Ou y a-t-il un modèle que nous avons oublié dans cette liste ? Faites-nous en part dans les commentaires ci-dessous !

Kumar Harsh

Kumar is a software developer and a technical author based in India. He specializes in JavaScript and DevOps. You can learn more about his work on his website.