Når du bygger JavaScript-applikationer, kan du støde på scenarier, hvor du skal bygge objekter på en bestemt, foruddefineret måde eller genbruge en fælles klasse ved at ændre eller tilpasse den til flere anvendelsestilfælde.

Det er naturligvis ikke praktisk at løse disse problemer igen og igen.

Det er her, JavaScript-designmønstre kommer dig til undsætning.

JavaScript-designmønstre giver dig en struktureret, gentagelig måde at løse almindeligt forekommende problemer i JavaScript-udviklingen på.

I denne vejledning vil vi se nærmere på, hvad JavaScript-designmønstre er, og hvordan du kan bruge dem i dine JavaScript-apps.

Hvad er et JavaScript-designmønster?

JavaScript-designmønstre er gentagelige skabelonløsninger til hyppigt forekommende problemer i JavaScript-appudvikling.

Ideen er enkel: Programmører over hele verden har siden udviklingens begyndelse stået over for sæt af tilbagevendende problemer, når de har udviklet apps. Med tiden har nogle udviklere valgt at dokumentere gennemprøvede måder at løse disse problemer på, så andre nemt kan henvise til løsningerne.

Efterhånden som flere og flere udviklere valgte at bruge disse løsninger og anerkendte deres effektivitet i løsningen af deres problemer, blev de accepteret som en standardmetode til problemløsning og fik navnet “designmønstre”

Efterhånden som betydningen af designmønstre blev bedre forstået, blev disse videreudviklet og standardiseret. De fleste moderne designmønstre har nu en defineret struktur, er organiseret under flere kategorier og undervises i datalogi-relaterede uddannelser som selvstændige emner.

Typer af JavaScript-designmønstre

Her er nogle af de mest populære klassifikationer af JavaScript-designmønstre.

Creational

Kreative designmønstre er mønstre, der hjælper med at løse problemer omkring oprettelse og håndtering af nye objektinstanser i JavaScript. Det kan være så simpelt som at begrænse en klasse til kun at have ét objekt eller så komplekst som at definere en indviklet metode til at håndplukke og tilføje hver enkelt funktion i et JavaScript-objekt.

Nogle eksempler på skabende designmønstre omfatter bl.a. Singleton, Factory, Abstract Factory og Builder.

Strukturel

Strukturelle designmønstre er mønstre, der hjælper med at løse problemer omkring håndtering af JavaScript-objekternes struktur (eller scheme). Disse problemer kan f.eks. være at skabe et forhold mellem to forskellige objekter eller at abstrahere nogle funktioner i et objekt for specifikke brugere.

Et par eksempler på strukturelle designmønstre er Adapter, Bridge, Composite og Facade.

Adfærdsmæssigt

Adfærdsmæssige designmønstre er mønstre, der hjælper med at løse problemer omkring, hvordan kontrol (og ansvar) overføres mellem forskellige objekter. Disse problemer kan omfatte kontrol af adgangen til en linket liste eller oprettelse af en enkelt enhed, der kan kontrollere adgangen til flere typer objekter.

Nogle eksempler på adfærdsmæssige designmønstre omfatter Command, Iterator, Memento og Observer.

Concurrency

Concurrency-designmønstre er mønstre, der hjælper med at løse problemer omkring multithreading og multitasking. Disse problemer kan omfatte opretholdelse af et aktivt objekt blandt flere tilgængelige objekter eller håndtering af flere begivenheder, der leveres til et system, ved at demultiplexe indgående input og håndtere det stykke for stykke.

Et par eksempler på samtidighedsdesignmønstre omfatter aktivt objekt, nuclear react og scheduler.

Arkitektonisk

Arkitektoniske designmønstre er mønstre, der hjælper med at løse problemer omkring softwaredesign i bred forstand. De er generelt relateret til, hvordan man designer sit system og sikrer høj tilgængelighed, mindsker risici og undgår flaskehalse i ydeevnen.

To eksempler på arkitektoniske designmønstre er MVC og MVVM.

Elementer i et designmønster

Næsten alle designmønstre kan opdeles i et sæt af fire vigtige komponenter. De er:

  • Mønsternavn: Dette bruges til at identificere et designmønster, mens der kommunikeres med andre brugere. Eksempler omfatter “singleton”, “prototype” og flere andre.
  • Problem: Dette beskriver formålet med designmønstret. Det er en lille beskrivelse af det problem, som designmønstret forsøger at løse. Det kan endda indeholde et eksempelscenarie for bedre at forklare problemet. Det kan også indeholde en liste over betingelser, der skal være opfyldt, for at et designmønster fuldt ud løser det underliggende problem.
  • Løsning: Dette er løsningen på det aktuelle problem, der består af elementer som klasser, metoder, grænseflader osv. Det er her, hovedparten af et designmønster ligger – det indebærer relationer, ansvar og samarbejdspartnere for forskellige elementer, som er klart defineret.
  • Resultater: Dette er en analyse af, hvor godt mønstret var i stand til at løse problemet. Ting som plads- og tidsforbrug diskuteres sammen med alternative tilgange til at løse det samme problem.

Hvis du ønsker at lære mere om designmønstre og deres tilblivelse, har MSU nogle kortfattede studiematerialer, som du kan henvise til.

Hvorfor skal du bruge designmønstre?

Der er flere grunde til, at du ønsker at bruge designmønstre:

  • De er gennemprøvede og testede: Med et designmønster har du en afprøvet løsning på dit problem (så længe designmønsteret passer til beskrivelsen af dit problem). Du behøver ikke at spilde tid på at lede efter alternative løsninger, og du kan være sikker på, at du har en løsning, der tager sig af den grundlæggende optimering af ydeevnen for dig.
  • De er nemme at forstå: Designmønstre er beregnet til at være små, enkle og lette at forstå. Du behøver ikke at være en specialiseret programmør, der har arbejdet i en bestemt branche i årtier for at forstå, hvilket designmønster du skal bruge. De er med vilje generiske (ikke begrænset til et bestemt programmeringssprog) og kan forstås af alle, der har tilstrækkelige problemløsningsevner. Det hjælper også, når der sker et skift i dit tekniske team: Et stykke kode, der er baseret på et designmønster, er lettere at forstå for enhver ny softwareudvikler.
  • De er enkle at implementere: De fleste designmønstre er meget enkle, som du vil se senere i vores artikel. Du behøver ikke at kende flere programmeringsbegreber for at implementere dem i din kode.
  • De foreslår en kodearkitektur, der er let genanvendelig: Genanvendelighed og renlighed af kode er stærkt opmuntret i hele den tekniske industri, og designmønstre kan hjælpe dig med at opnå dette. Da disse mønstre er en standardiseret måde at løse problemer på, har deres designere sørget for at sikre, at den omfattende app-arkitektur forbliver genanvendelig, fleksibel og kompatibel med de fleste former for kodeskrift.
  • De sparer tid og app-størrelse: En af de største fordele ved at stole på et standardsæt af løsninger er, at de hjælper dig med at spare tid, når du implementerer dem. Der er en god chance for, at hele dit udviklingsteam kender designmønstre godt, så det bliver lettere for dem at planlægge, kommunikere og samarbejde, når de implementerer dem. Afprøvede og testede løsninger betyder, at der er en god chance for, at du ikke ender med at lække ressourcer eller tage en omvej, mens du bygger en eller anden funktion, hvilket sparer dig både tid og plads. Desuden giver de fleste programmeringssprog dig standardskabelonbiblioteker, som allerede implementerer nogle almindelige designmønstre som Iterator og Observer.

Top 20 JavaScript-designmønstre, som du skal mestre

Nu hvor du forstår, hvad et designmønster består af, og hvorfor du har brug for dem, skal vi dykke dybere ned i, hvordan nogle af de mest almindeligt anvendte JavaScript-designmønstre kan implementeres i en JavaScript-app.

Creational

Lad os starte diskussionen med nogle grundlæggende, kreative designmønstre der er nemme at lære.

1. Singleton

Singleton-mønsteret er et af de mest almindeligt anvendte designmønstre i hele softwareudviklingsbranchen. Det problem, som det har til formål at løse, er kun at vedligeholde en enkelt instans af en klasse. Dette kan være praktisk, når der skal instantieres objekter, der er ressourcekrævende, f.eks. databasehåndterer.

Her er hvordan du kan implementere det i 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

Selv om det tjener formålet godt, er Singleton-mønstret kendt for at gøre debugging vanskelig, da det maskerer afhængigheder og kontrollerer adgangen til initialisering eller destruktion af en klasses instanser.

2. Factory

Factory-metoden er også et af de mest populære designmønstre. Det problem, som Factory-metoden har til formål at løse, er at skabe objekter uden at bruge den konventionelle konstruktør. I stedet tager den konfigurationen (eller beskrivelsen) af det ønskede objekt ind og returnerer det nyligt oprettede objekt.

Her er hvordan du kan implementere den i 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
*/

Factory-designmønsteret styrer, hvordan objekterne oprettes, og giver dig en hurtig måde at oprette nye objekter på samt en ensartet grænseflade, der definerer de egenskaber, som dine objekter skal have. Du kan tilføje lige så mange hunderacer, som du vil, men så længe de metoder og egenskaber, der udsættes af racetyperne, forbliver de samme, vil de fungere upåklageligt.

Bemærk dog, at Factory-mønsteret ofte kan føre til et stort antal klasser, som kan være vanskelige at administrere.

3. Abstrakt fabrik

Metoden Abstract Factory tager Factory-metoden et niveau op ved at gøre fabrikker abstrakte og dermed udskiftelige, uden at det kaldende miljø kender den nøjagtige anvendte fabrik eller dens interne arbejdsgange. Det kaldende miljø ved kun, at alle fabrikker har et sæt fælles metoder, som det kan kalde for at udføre instantieringshandlingen.

Sådan kan det implementeres ved hjælp af det foregående eksempel:

// 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

*/

Det abstrakte fabriksmønster gør det nemt for dig at udveksle konkrete fabrikker let, og det er med til at fremme ensartethed mellem fabrikker og de produkter, der oprettes. Det kan dog blive svært at introducere nye typer produkter, da du skal foretage ændringer i flere klasser for at få plads til nye metoder/egenskaber.

4. Builder

Builder-mønstret er et af de mest komplekse, men fleksible kreative JavaScript-designmønstre. Det giver dig mulighed for at bygge hver funktion ind i dit produkt en efter en, hvilket giver dig fuld kontrol over, hvordan dit objekt er bygget op, mens du stadig abstraherer de interne detaljer væk.

I det indviklede eksempel nedenfor kan du se Builder-designmønstret i aktion sammen med Director, der hjælper dig med at lave pizzaer!

// 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()

Du kan parre Builder med en Director, som det fremgår af klassen PizzaShop i eksemplet ovenfor, for på forhånd at definere et sæt trin, der skal følges hver gang for at bygge en standardvariant af dit produkt, dvs. en specifik opskrift på dine pizzaer.

Det eneste problem med dette designmønster er, at det er ret kompliceret at oprette og vedligeholde. Tilføjelse af nye funktioner på denne måde er dog enklere end Factory-metoden.

5. Prototype

Prototype-designmønstret er en hurtig og enkel måde at skabe nye objekter ud fra eksisterende objekter ved at klone dem.

Der oprettes først et prototypeobjekt, som kan klones flere gange for at oprette nye objekter. Det er praktisk, når det er mere ressourcekrævende at instantiere et objekt direkte end at oprette en kopi af et eksisterende objekt.

I eksemplet nedenfor kan du se, hvordan du kan bruge Prototype-mønsteret til at oprette nye dokumenter baseret på et fastsat skabelondokument:

// 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()

Prototype-metoden fungerer godt i tilfælde, hvor en stor del af dine objekter deler de samme værdier, eller når det er ret dyrt at oprette et nyt objekt i det hele taget. Det føles dog som overkill i tilfælde, hvor du ikke har brug for mere end et par instanser af klassen.

Strukturel

Strukturelle designmønstre hjælper dig med at organisere din forretningslogik ved at give dig gennemprøvede måder at strukturere dine klasser på. Der findes en række forskellige strukturelle designmønstre, som hver især tager højde for unikke brugssituationer.

6. Adapter

Et almindeligt problem, når man bygger apps, er at tillade samarbejde mellem inkompatible klasser.

Et godt eksempel til at forstå dette er samtidig med at man opretholder bagudkompatibilitet. Hvis du skriver en ny version af en klasse, vil du naturligvis gerne have, at den let kan bruges alle de steder, hvor den gamle version fungerede. Hvis du imidlertid foretager brudte ændringer som f.eks. fjernelse eller opdatering af metoder, der var afgørende for, at den gamle version fungerede, kan du ende med en klasse, som kræver, at alle dens klienter skal opdateres for at kunne køre.

I sådanne tilfælde kan Adapter-designmønstret hjælpe.

Adapter-designmønstret giver dig en abstraktion, der bygger bro over kløften mellem den nye klasses metoder og egenskaber og den gamle klasses metoder og egenskaber. Den har den samme grænseflade som den gamle klasse, men den indeholder logik til at mappe gamle metoder til de nye metoder for at udføre lignende operationer. Dette svarer til, hvordan en stikkontakt fungerer som en adapter mellem et amerikansk og et europæisk stik.

Her er et eksempel:

// 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()

Det største problem med dette designmønster er, at det tilføjer kompleksitet til din kildekode. Du skulle allerede vedligeholde to forskellige klasser, og nu har du endnu en klasse – Adapteren – at vedligeholde.

7. Bridge

Bridge-designmønsteret udvider Adapter-mønsteret og giver både klassen og klienten separate grænseflader, så de begge kan fungere, selv i tilfælde af inkompatible native grænseflader.

Det hjælper med at udvikle en meget løst koblet grænseflade mellem de to typer objekter. Det er også med til at forbedre udvidelsesmulighederne for grænsefladerne og deres implementeringer for at opnå maksimal fleksibilitet.

Sådan kan du bruge det:

// 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()
}

Som du måske allerede har gættet, øger Bridge-mønsteret kompleksiteten af kodebasen betydeligt. Desuden ender de fleste grænseflader normalt med kun én implementering i virkelige brugstilfælde, så du har ikke rigtig meget gavn af kodegenbrugeligheden.

8. Composite

Designmønstret Composite hjælper dig med at strukturere og administrere lignende objekter og enheder på en nem måde. Den grundlæggende idé bag Composite-mønstret er, at objekter og deres logiske containere kan repræsenteres ved hjælp af en enkelt abstrakt klasse (som kan gemme data/metoder relateret til objektet og referencer til sig selv for containerens vedkommende).

Det giver mest mening at bruge Composite-mønstret, når din datamodel ligner en træstruktur. Du bør dog ikke forsøge at omdanne en datamodel uden træer til en trælignende datamodel blot for at bruge Composite-mønsteret, da det ofte kan fjerne en stor del af fleksibiliteten.

I eksemplet nedenfor kan du se, hvordan du kan bruge Composite-designmønsteret til at konstruere et emballagesystem til e-handelsprodukter, der også kan beregne den samlede ordreværdi pr. pakke:

// 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()

Den største ulempe ved at bruge Composite-mønstret er, at ændringer af komponentgrænsefladerne kan være meget udfordrende i fremtiden. Det tager tid og kræfter at designe grænsefladerne, og datamodellens træagtige karakter kan gøre det meget vanskeligt at foretage de ændringer, man ønsker.

9. Decorator

Decorator-mønstret hjælper dig med at tilføje nye funktioner til eksisterende objekter ved blot at pakke dem ind i et nyt objekt. Det svarer til, hvordan du kan pakke en allerede indpakket gaveæske ind i nyt gavepapir så mange gange, du vil: Hver indpakning giver dig mulighed for at tilføje så mange funktioner, som du ønsker, så det er fantastisk på fleksibilitetsfronten.

Ud fra et teknisk perspektiv er der ingen arv involveret, så der er større frihed ved udformning af forretningslogik.

I eksemplet nedenfor kan du se, hvordan Decorator-mønstret hjælper med at tilføje flere funktioner til en standardklasse 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()

Ulemperne ved dette mønster er bl.a., at koden er meget kompleks, da der ikke er defineret noget standardmønster for tilføjelse af nye funktioner ved hjælp af decorators. Du kan ende op med en masse ikke-uniforme og/eller lignende decorators i slutningen af din softwareudviklings livscyklus.

Hvis du ikke er forsigtig med at designe decorators, kan du ende med at designe nogle decorators til at være logisk afhængige af andre. Hvis dette ikke løses, kan fjernelse eller omstrukturering af decorators senere hen i processen ødelægge stabiliteten af din applikation.

10. Facade

Når man bygger de fleste reelle applikationer, viser forretningslogikken sig normalt at være ret kompleks, når man er færdig. Du kan ende med at have flere objekter og metoder, der er involveret i udførelsen af kerneoperationer i din app. Det kan være ret vanskeligt og fejlbehæftet at holde styr på deres initialiseringer, afhængigheder, den korrekte rækkefølge for udførelse af metoder osv., hvis det ikke gøres korrekt.

Facade-designmønstret hjælper dig med at skabe en abstraktion mellem det miljø, der påkalder de ovennævnte operationer, og de objekter og metoder, der er involveret i udførelsen af disse operationer. Denne abstraktion indeholder logikken til initialisering af objekterne, sporing af deres afhængigheder og andre vigtige aktiviteter. Det kaldende miljø har ingen oplysninger om, hvordan en operation udføres. Du kan frit opdatere logikken uden at foretage ændringer i den kaldende klient.

Her er hvordan du kan bruge det i en applikation:

/**
* 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()
}

En ulempe ved at bruge Facade-mønstret er, at det tilføjer et ekstra abstraktionslag mellem din forretningslogik og klienten, hvilket kræver yderligere vedligeholdelse. Oftest øger dette den samlede kompleksitet af kodebasen.

Derudover bliver Facade -klassen en obligatorisk afhængighed af din app’s funktion – hvilket betyder, at eventuelle fejl i Facade -klassen har direkte indflydelse på din app’s funktion.

11. Flyweight

Flyweight-mønstret hjælper dig med at løse problemer, der involverer objekter med gentagende komponenter, på en hukommelseseffektiv måde ved at hjælpe dig med at genbruge de fælles komponenter i din objektpulje. Dette er med til at reducere belastningen af hukommelsen og resulterer også i hurtigere udførelsestider.

I eksemplet nedenfor gemmes en stor sætning i hukommelsen ved hjælp af Flyweight-designmønstret. I stedet for at lagre hvert enkelt tegn, efterhånden som det forekommer, identificerer programmet det sæt af forskellige tegn, der er blevet brugt til at skrive afsnittet, og deres typer (tal eller alfabet) og opbygger genanvendelige flyweights for hvert tegn, der indeholder oplysninger om, hvilket tegn og hvilken type der er gemt.

Derefter lagrer hovedarrayet blot en liste over referencer til disse flyweights i den rækkefølge, de forekommer i sætningen, i stedet for at lagre en instans af karakterobjektet, hver gang det forekommer.

Dette reducerer den hukommelse, som sætningen bruger, med halvdelen. Husk på, at dette er en meget grundlæggende forklaring på, hvordan tekstprocessorer lagrer tekst.

// 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()

Som du måske allerede har bemærket, øger Flyweight-mønsteret kompleksiteten af dit softwaredesign ved ikke at være særlig intuitivt. Så hvis det ikke er et presserende problem for din app at spare hukommelse, kan Flyweight’s ekstra kompleksitet gøre mere skade end gavn.

Desuden bytter Flyweights hukommelse for behandlingseffektivitet, så hvis du mangler CPU-cyklusser, er Flyweight ikke en god løsning for dig.

12. Proxy

Proxy-mønstret hjælper dig med at erstatte et objekt med et andet objekt. Med andre ord kan proxy-objekter erstatte faktiske objekter (som de er en proxy for) og kontrollere adgangen til objektet. Disse proxyobjekter kan bruges til at udføre nogle handlinger før eller efter, at en invocationforespørgsel er sendt videre til det faktiske objekt.

I eksemplet nedenfor kan du se, hvordan adgangen til en databaseinstans styres via en proxy, der udfører nogle grundlæggende valideringskontroller af anmodningerne, før de tillades:

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()

Dette designmønster er almindeligt anvendt i hele branchen og hjælper med at implementere før- og efterudførelsesoperationer nemt. Men ligesom alle andre designmønstre tilføjer det også kompleksitet til din kodebase, så prøv at lade være med at bruge det, hvis du ikke virkelig har brug for det.

Du skal også huske på, at da der er et ekstra objekt involveret, når du foretager kald til dit egentlige objekt, kan der være en vis latenstid på grund af de ekstra behandlingsoperationer. Optimering af dit hovedobjekts ydeevne indebærer nu også optimering af din proxys metoder med henblik på ydeevne.

Adfærdsmæssig

Adfærdsmæssige designmønstre hjælper dig med at løse problemer omkring, hvordan objekter interagerer med hinanden. Dette kan involvere deling eller videregivelse af ansvar/kontrol mellem objekter for at gennemføre sætoperationer. Det kan også involvere videregivelse/deling af data på tværs af flere objekter på den mest effektive måde muligt.

13. Ansvarskæde

Chain of Responsibility-mønstret er et af de enkleste adfærdsmæssige designmønstre. Det er praktisk, når du designer logik for operationer, der kan håndteres af flere handlers.

I lighed med hvordan problemeskalering fungerer i supportteams, sendes kontrollen gennem en kæde af handlere, og den handler, der er ansvarlig for at foretage handlingen, fuldfører operationen. Dette designmønster bruges ofte i UI-design, hvor flere lag af komponenter kan håndtere en brugerinputbegivenhed, f.eks. et tryk eller et swipe.

Nedenfor kan du se et eksempel på en klageskalering, der anvender ansvarskædemønstret. Klagen vil blive håndteret af behandlerne på grundlag af dens alvorlighed:

// 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()

Det indlysende problem med dette design er, at det er lineært, så der kan være en vis latenstid i håndteringen af en operation, når et stort antal handlere er kædet sammen.

At holde styr på alle handlers kan være et andet smertepunkt, da det kan blive ret rodet efter et vist antal handlers. Debugging er endnu et mareridt, da hver anmodning kan ende på en anden handler, hvilket gør det svært for dig at standardisere lognings- og debuggingprocessen.

14. Iterator

Iterator-mønstret er ret simpelt og bruges meget ofte i næsten alle moderne objektorienterede sprog. Hvis du står over for at skulle gennemgå en liste af objekter, der ikke alle er af samme type, kan normale iterationsmetoder, såsom for-loops, blive ret rodet – især hvis du også skriver forretningslogik indeni dem.

Iterator-mønstret kan hjælpe dig med at isolere iterations- og behandlingslogikken for dine lister fra den primære forretningslogik.

Her kan du se, hvordan du kan bruge det på en ret grundlæggende liste med flere typer elementer:

// 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()

Det siger sig selv, at dette mønster kan være unødvendigt komplekst for lister uden flere typer af elementer. Hvis der er for mange typer af elementer i en liste, kan den også blive vanskelig at administrere.

Nøglen er at identificere, om du virkelig har brug for en iterator baseret på din liste og dens fremtidige ændringsmuligheder. Desuden er Iterator-mønstret kun nyttigt i lister, og lister kan nogle gange begrænse dig til deres lineære adgangsform. Andre datastrukturer kan nogle gange give dig større ydelsesfordele.

15. Mediator

Dit applikationsdesign kan nogle gange kræve, at du skal lege med et stort antal forskellige objekter, der rummer forskellige former for forretningslogik og ofte er afhængige af hinanden. Håndteringen af afhængighederne kan nogle gange blive vanskelig, da du skal holde styr på, hvordan disse objekter udveksler data og kontrol mellem dem.

Mediator-designmønstret har til formål at hjælpe dig med at løse dette problem ved at isolere interaktionslogikken for disse objekter i et separat objekt for sig selv.

Dette separate objekt er kendt som mediator og er ansvarlig for at få arbejdet udført af dine klasser på lavere niveau. Din klient eller det kaldende miljø vil også interagere med mediatoren i stedet for klasserne på lavere niveau.

Her er et eksempel på mediator-designmønsteret i praksis:

// 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()

Mens mediatoren giver dit app-design afkobling og en stor fleksibilitet, er det i sidste ende endnu en klasse, som du skal vedligeholde. Du skal vurdere, om dit design virkelig kan drage fordel af en mediator, før du skriver en, så du ikke ender med at tilføje unødig kompleksitet til din kodebase.

Det er også vigtigt at huske på, at selv om mediatorklassen ikke indeholder nogen direkte forretningslogik, indeholder den stadig en masse kode, der er afgørende for, at din app fungerer, og som derfor hurtigt kan blive ret kompleks.

16. Memento

Versionering af objekter er et andet almindeligt problem, som du vil støde på, når du udvikler apps. Der er mange anvendelsestilfælde, hvor du har brug for at vedligeholde et objekts historik, understøtte nemme rollbacks og nogle gange endda understøtte tilbageførsel af disse rollbacks. Det kan være svært at skrive logikken til sådanne apps.

Memento-designmønstret er beregnet til at løse dette problem nemt.

Et memento betragtes som et øjebliksbillede af et objekt på et bestemt tidspunkt. Memento-designmønstret gør brug af disse mementoer til at bevare øjebliksbilleder af objektet, efterhånden som det ændres over tid. Når du har brug for at rulle tilbage til en gammel version, kan du blot hente memento’et for det.

Her er, hvordan du kan implementere det i en tekstbehandlingsapp:

// 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()

Memento-designmønsteret er en god løsning til at administrere et objekts historik, men det kan blive meget ressourcekrævende. Da hver memento næsten er en kopi af objektet, kan det meget hurtigt fylde din app’s hukommelse, hvis det ikke bruges med måde.

Med et stort antal objekter kan deres livscyklusstyring også blive en ret kedelig opgave. Oven i alt dette er Originator – og Caretaker -klasserne normalt meget tæt koblet, hvilket øger kompleksiteten af din kodebase.

17. Observer

Observer-mønstret giver en alternativ løsning på problemet med interaktion mellem flere objekter (som tidligere er set i Mediator-mønstret).

I stedet for at lade hvert objekt kommunikere med hinanden gennem en udpeget mediator, giver Observer-mønstret dem mulighed for at observere hinanden. Objekter er designet til at udsende begivenheder, når de forsøger at sende data eller kontrol, og andre objekter, der “lytter” til disse begivenheder, kan modtage dem og interagere på grundlag af deres indhold.

Her er en simpel demonstration af udsendelse af nyhedsbreve til flere personer via Observer-mønsteret:

// 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()

Selvom Observer-mønstret er en smart måde at sende kontrol og data rundt på, er det bedre egnet til situationer, hvor der er et stort antal afsendere og modtagere, der interagerer med hinanden via et begrænset antal forbindelser. Hvis objekterne alle skulle lave en-til-en-forbindelser, ville man miste den fordel, man får ved at udgive og abonnere på begivenheder, da der altid kun vil være én abonnent for hver udgiver (når det ville være bedre håndteret med en direkte kommunikationslinje mellem dem).

Desuden kan Observer-designmønstret føre til problemer med ydeevnen, hvis abonnementsbegivenhederne ikke håndteres korrekt. Hvis et objekt fortsætter med at abonnere på et andet objekt, selv når det ikke er nødvendigt, vil det ikke være berettiget til garbage collection og vil øge appens hukommelsesforbrug.

18. Tilstand

State-designmønstret er et af de mest anvendte designmønstre i hele softwareudviklingsindustrien. Populære JavaScript-frameworks som React og Angular er stærkt afhængige af State-mønstret til at administrere data og appadfærd baseret på disse data.

Kort sagt er State-designmønstret nyttigt i situationer, hvor du kan definere definitive tilstande for en enhed (som kan være en komponent, en side, en app eller en maskine), og hvor enheden har en foruddefineret reaktion på tilstandsændringen.

Lad os sige, at du forsøger at opbygge en låneansøgningsproces. Hvert trin i ansøgningsprocessen kan defineres som en tilstand.

Mens kunden normalt ser en lille liste over forenklede tilstande for deres ansøgning (afventende, under gennemgang, accepteret og afvist), kan der internt være andre trin involveret. På hvert af disse trin vil ansøgningen blive tildelt en bestemt person og kan have unikke krav.

Systemet er udformet på en sådan måde, at når behandlingen i en tilstand er afsluttet, opdateres tilstanden til den næste i rækken, og det næste relevante sæt trin påbegyndes.

Her er hvordan du kan opbygge et opgavestyringssystem ved hjælp af State-designmønstret:

// 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()

Selv om State-mønstret gør et godt stykke arbejde med at adskille trin i en proces, kan det blive ekstremt vanskeligt at vedligeholde i store applikationer, der har flere tilstande.

Hvis dit procesdesign desuden tillader mere end blot at bevæge sig lineært gennem alle tilstande, skal du skrive og vedligeholde mere kode, da hver tilstandsovergang skal håndteres separat.

19. Strategi

Strategy-mønstret, der også er kendt som Policy-mønsteret, har til formål at hjælpe dig med at indkapsle og frit udveksle klasser ved hjælp af en fælles grænseflade. Dette hjælper med at opretholde en løs kobling mellem klienten og klasserne og giver dig mulighed for at tilføje så mange implementeringer, som du ønsker.

Strategy-mønstret er kendt for at være en enorm hjælp i situationer, hvor der er behov for den samme operation ved hjælp af forskellige metoder/algoritmer, eller hvor massive switch-blokke skal erstattes med mere menneskevenlig kode.

Her er et eksempel på Strategy-mønstret:

// 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()

Strategy-mønstret er fantastisk, når det drejer sig om at indføre nye variationer af en enhed uden at ændre klienterne meget. Det kan dog virke som overkill, hvis du kun har en håndfuld variationer, der skal implementeres.

Desuden fjerner indkapslingen finere detaljer om hver enkelt variations interne logik, så din klient ikke er klar over, hvordan en variant vil opføre sig.

20. Besøgende

Visitor-mønstret har til formål at hjælpe dig med at gøre din kode udvidelig.

Ideen er at stille en metode til rådighed i klassen, der gør det muligt for objekter fra andre klasser at foretage ændringer i objekter fra den aktuelle klasse på en nem måde. De andre objekter besøger det aktuelle objekt (også kaldet stedobjektet), eller den aktuelle klasse accepterer besøgsobjekterne, og stedobjektet håndterer besøget af hvert eksternt objekt på passende vis.

Sådan kan du bruge det:

// 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()

Den eneste fejl ved dette design er, at hver besøgsklasse skal opdateres, når der tilføjes eller ændres et nyt sted. I tilfælde, hvor der findes flere besøgende og stedobjekter sammen, kan det være svært at vedligeholde.

Bortset fra det fungerer metoden godt til at forbedre funktionaliteten af klasser dynamisk.

Bedste praksis for implementering af designmønstre

Nu hvor du har set de mest almindelige designmønstre på tværs af JavaScript, er her nogle tips, som du bør huske på, når du implementerer dem.

Vær særlig omhyggelig med at forstå, om et mønster passer til løsningen

Dette tip skal anvendes, før du implementerer et designmønster i din kildekode. Selv om det kan se ud som om et designmønster er afslutningen på alle dine bekymringer, skal du tage et øjeblik til at analysere kritisk, om det er sandt.

Der er mange mønstre, der løser det samme problem, men som har forskellige tilgange og har forskellige konsekvenser. Så dine kriterier for valg af et designmønster bør ikke kun være, om det løser dit problem eller ej – det bør også være, hvor godt det løser dit problem, og om der er et andet mønster, der kan præsentere en mere effektiv løsning.

Forstå omkostningerne ved at implementere et mønster, før du går i gang

Selv om designmønstre synes at være den bedste løsning på alle tekniske problemer, bør du ikke kaste dig ud i at implementere dem i din kildekode med det samme.

Mens du vurderer konsekvenserne af at implementere en løsning, skal du også tage hensyn til din egen situation. Har du et stort team af softwareudviklere, der er velbevandret i at forstå og vedligeholde designmønstre? Eller er du en stifter i en tidlig fase med et minimalt udviklingsteam, der ønsker at frigive en hurtig MVP af dit produkt? Hvis du svarer ja til det sidste spørgsmål, er designmønstre måske ikke den mest optimale udviklingsmetode for dig.

Designmønstre fører ikke til omfattende genbrug af kode, medmindre de planlægges i en meget tidlig fase af appdesignet. Hvis du tilfældigt bruger designmønstre i forskellige faser, kan det føre til en unødigt kompleks app-arkitektur, som du skal bruge uger på at forenkle.

Effektiviteten af et designmønster kan ikke bedømmes ved hjælp af nogen form for test. Det er dit teams erfaring og selvransagelse, der vil lade dig vide, om de virker. Hvis du har tid og ressourcer til at afsætte til disse aspekter, er det først da, at designmønstre virkelig vil løse dine problemer.

Gør ikke alle løsninger til et mønster

En anden tommelfingerregel, som du skal huske på, er at afstå fra at forsøge at gøre hvert eneste lille problem-løsningspar til et designmønster og bruge det, hvor du ser plads til det.

Selv om det er godt at identificere standardløsninger og huske dem, når du støder på lignende problemer, er der en god chance for, at det nye problem, du er stødt på, ikke passer til nøjagtig den samme beskrivelse som et ældre problem. I så fald kan du ende med at implementere en suboptimal løsning og spilde ressourcer.

Designmønstre er i dag etableret som førende eksempler på problem-løsningspar, fordi de er blevet afprøvet af hundred- og tusindvis af programmører gennem tiden og er blevet generaliseret så meget som muligt. Hvis du forsøger at gentage denne indsats ved blot at se på en masse problemer og løsninger og kalde dem ens, kan du ende med at gøre meget mere skade på din kode, end du nogensinde havde forventet.

Hvornår skal du bruge designmønstre?

For at opsummere er her et par stikord, som du bør holde øje med for at bruge designmønstre. De gælder ikke alle sammen for udviklingen af alle apps, men de burde give dig en god idé om, hvad du skal være opmærksom på, når du overvejer at bruge designmønstre:

  • Du har et stærkt internt team af udviklere, der har en god forståelse for designmønstre.
  • Du følger en SDLC-model, der giver plads til dybdegående diskussioner om arkitekturen af din app, og designmønstre er blevet nævnt i disse diskussioner.
  • Det samme sæt af problemer er dukket op flere gange i dine designdiskussioner, og du kender det designmønster, der passer til sagen.
  • Du har forsøgt at løse en mindre variation af dit problem uafhængigt af designmønstret.
  • Med designmønsteret på plads ser din kode ikke alt for kompleks ud.

Hvis et designmønster løser dit problem og hjælper dig med at skrive kode, der er enkel, genanvendelig, modulær, løst koblet og fri for “kodelugt”, er det måske den rigtige vej at gå.

Et andet godt tip at huske på er at undgå at gøre alting til et designmønster. Designmønstre er beregnet til at hjælpe dig med at løse problemer. De er ikke love, som du skal overholde, eller regler, som du skal følge strengt. De ultimative regler og love er stadig de samme: Hold din kode ren, enkel, læsbar og skalerbar. Hvis et designmønster hjælper dig med at gøre dette, samtidig med at det løser dit problem, bør du sagtens kunne køre med det.

Opsummering

JavaScript-designmønstre er en vidunderlig måde at gribe problemer an på, som flere programmører har stået over for i tidens løb. De præsenterer gennemprøvede løsninger, der bestræber sig på at holde din kodebase ren og løst koblet.

I dag er der hundredvis af designmønstre til rådighed, som løser næsten alle de problemer, du støder på, når du bygger apps. Det er dog ikke alle designmønstre, der virkelig vil løse dit problem hver gang.

Ligesom enhver anden programmeringskonvention skal designmønstre opfattes som forslag til løsning af problemer. De er ikke love, der skal følges hele tiden, og hvis du behandler dem som love, kan du ende med at gøre stor skade på dine apps.

Når din app er færdig, har du brug for et sted at hoste den – og Kinstas løsninger til applikationshosting er førende blandt de hurtigste, mest pålidelige og mest sikre. Du skal blot logge ind på din MyKinsta-konto (Kinstas brugerdefinerede administrative dashboard), oprette forbindelse til dit GitHub-repositorium og starte! Desuden bliver du kun opkrævet for de ressourcer, som din app bruger.

Hvilke designmønstre bruger du regelmæssigt i dit job som softwareprogrammør? Eller er der et mønster, som vi har overset på listen? Lad os vide det i kommentarerne nedenfor!

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.