När du bygger JavaScript-applikationer så kan du stöta på scenarier där du måste bygga objekt på ett visst, fördefinierat sätt. Det kan även hända att du måste återanvända en gemensam klass genom att ändra eller anpassa den till flera användningsområden.

Det är naturligtvis inte bekvämt att lösa dessa problem om och om igen.

Det är här som JavaScript-designmönster kommer in i bilden.

JavaScript-designmönster ger dig ett strukturerat, upprepningsbart sätt att ta itu med vanligt förekommande problem vid JavaScript-utveckling.

I den här guiden så tar vi en titt på vad JavaScript-designmönster är och hur du kan använda dem i dina JavaScript-appar.

Vad är ett JavaScript-designmönster?

JavaScript-designmönster är upprepningsbara malllösningar för ofta förekommande problem vid utveckling av JavaScript-appar.

Idén är enkel: Programmerare över hela världen har sedan utvecklingens början ställts inför återkommande problem när de utvecklar appar. Med tiden så valde vissa utvecklare därför att dokumentera beprövade och testade sätt att hantera dessa problem. Som ett resultat så kunde andra utvecklare hänvisa till dessa lösningar.

Fler och fler utvecklare valde att använda dessa lösningar och insåg att de var effektiva när det gällde att lösa problemen. De blev därmed accepterade som ett standardiserat sätt att lösa problem och fick namnet ”designmönster”

I takt med att betydelsen av designmönster blev bättre förstådd så utvecklades och standardiserades dessa ytterligare. De flesta moderna designmönster har numera en definierad struktur och är organiserade i flera kategorier. De undervisas dessutom i datavetenskapsrelaterade utbildningar som fristående ämnen.

Typer av JavaScript-designmönster

Här är några av de mest populära klassificeringarna av JavaScript-designmönster.

Skapande

Kreativa designmönster är sådana som hjälper till att lösa problem kring skapande och hantering av nya objektinstanser i JavaScript. Det kan vara så enkelt som att begränsa en klass till att bara ha ett objekt.  Processen kan dock även vara så komplex som att definiera en intrikat metod för att handplocka och lägga till varje funktion i ett JavaScript-objekt.

Några exempel på skapande designmönster är bland annat Singleton, Factory, Abstract Factory och Builder.

Strukturellt

Strukturella designmönster är sådana som hjälper till att lösa problem kring hantering av JavaScript-objektens struktur (eller schema). Dessa problem kan vara att skapa en relation mellan två olika objekt eller att abstrahera vissa funktioner i ett objekt för specifika användare.

Några exempel på strukturella designmönster är Adapter, Bridge, Composite och Facade.

Beteendemönster

Beteendemässiga designmönster är sådana som hjälper till att lösa problem med hur kontroll (och ansvar) överförs mellan olika objekt. Dessa problem kan handla om att kontrollera åtkomsten till en länkad lista eller att skapa en enda enhet som kan kontrollera åtkomsten till flera typer av objekt.

Några exempel på beteendemönster är Command, Iterator, Memento och Observer.

Samtidighet

Designmönster för samtidighet är sådana som hjälper till att lösa problem som rör flertrådning och multitasking. Dessa problem kan innebära att man måste behålla ett aktivt objekt bland flera tillgängliga objekt. Det kan även hända att man måste hantera flera händelser som levereras till ett system genom att dela upp en inkommande inmatning och hantera den bit för bit.

Några exempel på designmönster för samtidighet är Active Object, Core Reactor och Scheduler.

Arkitektonisk

Arkitektoniska designmönster är sådana som hjälper till att lösa problem med programvarudesign i bred bemärkelse. Dessa är i allmänhet relaterade till hur man utformar sitt system och säkerställer hög tillgänglighet, begränsar risker och undviker flaskhalsar i prestanda.

Två exempel på arkitektoniska designmönster är MVC och MVVM.

Element i ett designmönster

Nästan alla designmönster kan delas upp i en uppsättning av fyra viktiga komponenter. Dessa är:

  • Mönsternamn: Detta används för att identifiera ett designmönster när man kommunicerar med andra användare. Mönsternamn är exempelvis ”singleton”, ”prototyp” med flera.
  • Problem: Här beskrivs syftet med designmönstret. Det är en liten beskrivning av det problem som designmönstret försöker lösa. Det kan även inkludera ett exempelscenario för att bättre förklara problemet. Utöver detta så kan det dessutom innehålla en lista över villkor som måste uppfyllas för att ett designmönster ska lösa det underliggande problemet fullt ut.
  • Lösning: Detta är lösningen på det aktuella problemet, som består av element som klasser, metoder, gränssnitt osv. Det är här som huvuddelen av ett designmönster ligger – det innehåller relationer, ansvar och samarbetspartners för olika element som är tydligt definierade.
  • Resultat: Detta är en analys av hur väl mönstret kunde lösa problemet. Saker som utrymmes- och tidsanvändning diskuteras, tillsammans med alternativa tillvägagångssätt för att lösa samma problem.

Om du vill lära dig mer om designmönster och deras tillkomst så har MSU ett kortfattat studiematerial som du kan använda.

Varför ska du använda designmönster?

Det finns flera anledningar till varför du bör använda designmönster:

  • De är beprövade och testade: Med ett designmönster så har du en beprövad och testad lösning på ditt problem (så länge som designmönstret passar in på beskrivningen av ditt problem). Du behöver inte slösa tid på att leta efter alternativa lösningar, och du kan vara säker på att du har en lösning som tar hand om grundläggande prestandaoptimering åt dig.
  • De är lätta att förstå: Designmönster är avsedda att vara små, enkla och lätta att förstå. Du behöver därför inte vara en specialiserad programmerare som har arbetat i en specifik bransch i årtionden för att förstå vilket designmönster som du ska använda. De är medvetet generiska (inte begränsade till något särskilt programmeringsspråk). Som ett resultat så kan de förstås av alla som har tillräckliga problemlösningsförmågor. Detta underlättar även när du har ett byte av ägare i ditt tekniska team: En kod som bygger på ett designmönster är lättare att förstå för en ny programvaruutvecklare.
  • De är enkla att implementera: De flesta designmönster är mycket enkla, vilket du kommer att se senare i vår artikel. Du behöver inte känna till flera programmeringskoncept för att implementera dem i din kod.
  • De föreslår en kodarkitektur som lätt kan återanvändas: Återanvändbarhet och renhet i koden uppmuntras starkt i hela teknikbranschen. Designmönster kan hjälpa dig att uppnå detta. Dessa mönster är ett standardiserat sätt att lösa problem. Därför så har deras designers sett till att den omslutande apparkitekturen förblir återanvändbar, flexibel och kompatibel med de flesta former av kodskrivning.
  • De sparar tid och sparar appstorlek: En av de största fördelarna med att förlita sig på en standard-uppsättning av lösningar är att de hjälper dig att spara tid när du implementerar dem. Det finns en god chans att hela ditt utvecklingsteam känner till designmönster väl. Som ett resultat så blir det lättare för dem att planera, kommunicera och samarbeta när de implementerar dem. Beprövade och testade lösningar innebär att det finns en god chans att ni inte kommer att läcka några resurser eller ta en omväg när ni bygger någon funktion. Detta sparar både tid och utrymme. Dessutom så erbjuder de flesta programmeringsspråken standardmallbibliotek som redan implementerar några vanliga designmönster som Iterator och Observer.

De 20 bästa designmönstren för JavaScript som du bör behärska

Nu förstår du vad ett designmönster består av och varför du behöver dem. Det är dags att djupdyka i hur några av de vanligaste JavaScript-designmönstren kan implementeras i en JavaScript-app.

Skapande

Låt oss börja diskussionen med några grundläggande, lättlärda kreativa designmönster.

1. Singleton

Singleton-mönstret är ett av de mest använda designmönstren i hela mjukvaruutvecklingsbranschen. Det problem som det syftar till att lösa är att endast upprätthålla en enda instans av en klass. Detta kan exempelvis vara praktiskt när man instansierar objekt som är resurskrävande, t.ex. databashanterare.

Så här kan du implementera detta 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

Singleton-mönstret är känt för att försvåra felsökning eftersom det maskerar beroenden och kontrollerar åtkomsten till initialisering eller förstöring av en klassinstans.

2. Factory

Factory-metoden är också ett av de mest populära designmönstren. Det problem som Factory-metoden syftar till att lösa är att skapa objekt utan att använda den konventionella konstruktören. Den tar istället in konfigurationen (eller beskrivningen) av det objekt som du vill ha och returnerar det nyskapade objektet.

Så här kan du implementera 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önstret styr hur objekten ska skapas och ger dig ett snabbt sätt att skapa nya objekt. Du får dessutom ett enhetligt gränssnitt som definierar de egenskaper som dina objekt ska ha. Det går att lägga till hur många hundraser som helst. Så länge som metoderna och egenskaperna som rastyperna exponerar förblir desamma kommer de att fungera felfritt ändå.

Observera dock att Factory-mönstret ofta kan leda till ett stort antal klasser som kan vara svåra att hantera.

3. Abstract Factory

Metoden Abstract Factory tar upp Factory-metoden en nivå genom att göra factories abstrakta och därmed utbytbara. Detta sker utan att den anropande miljön känner till exakt vilken factory som används eller hur den fungerar internt. Den anropande miljön vet bara att alla factories har en uppsättning gemensamma metoder som den kan anropa för att utföra instans-åtgärden.

Så här kan detta genomföras med hjälp av det tidigare exemplet:

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

*/

Abstract Factory-mönstret gör det enkelt för dig att utbyta konkreta factories. Det bidrar dessutom till att främja enhetlighet mellan factories och de produkter som skapas. Det kan dock bli svårt att införa nya typer av produkter eftersom du måste göra ändringar i flera klasser för att få plats med nya metoder/egenskaper.

4. Builder

Builder-mönstret är ett av de mest komplexa men flexibla skapande JavaScript-designmönstren. Det låter dig bygga in varje funktion i din produkt en efter en. Som ett resultat så får du full kontroll över hur ditt objekt byggs samtidigt som du abstraherar bort de interna detaljerna.

I det invecklade exemplet nedan så ser du Builder-designmönstret i aktion tillsammans med Director som hjälper dig att göra pizzor!

// 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 koppla ihop Builder med en Director, vilket visas av klassen PizzaShop i exemplet ovan. Som ett resultat så kan du definiera en uppsättning steg som ska följas i förväg varje gång som du bygger en standardvariant av din produkt. I detta fallet så gäller det ett specifikt recept för dina pizzor.

Det enda problemet med det här designmönstret är att det är ganska komplicerat att inrätta och underhålla. Att lägga till nya funktioner på detta sätt är dock enklare än Factory-metoden.

5. Prototyp

Designmönstret Prototype är ett snabbt och enkelt sätt att skapa nya objekt från befintliga objekt genom att klona dem.

Först så skapas ett prototyp-objekt som kan klonas flera gånger för att skapa nya objekt. Det är praktiskt när det är mer resurskrävande att direkt instansiera ett objekt än att skapa en kopia av ett befintligt objekt.

I exemplet nedan så ser du hur du kan använda prototype-mönstret för att skapa nya dokument baserat på ett inställt malldokument:

// 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 fungerar utmärkt i fall där en stor del av dina objekt delar samma värden. Den är även lämplig när det är ganska kostsamt att skapa ett nytt objekt helt och hållet. Det känns dock som en överdrift i de fall där du inte behöver mer än några få instanser av klassen.

Strukturellt

Strukturella designmönster hjälper dig att organisera din affärslogik genom att tillhandahålla beprövade sätt att strukturera dina klasser. Det finns en mängd olika strukturella designmönster som alla tillgodoser unika användningsområden.

6. Adapter

Ett vanligt problem när man bygger appar är att tillåta samarbete mellan inkompatibla klasser.

Ett bra exempel för att förstå detta är att bibehålla bakåtkompatibilitet. Om du skriver en ny version av en klass så vill du naturligtvis att den ska vara lätt att använda på alla ställen där den gamla versionen fungerade. Men det kan ju hända att du gör brytande ändringar som att ta bort eller uppdatera metoder som var avgörande för att den gamla versionen skulle fungera. Då kan det hända att du får en klass som kräver att alla dess klienter uppdateras för att kunna köras.

I sådana fall kan designmönstret Adapter hjälpa till.

Designmönstret Adapter ger dig en abstraktion som överbryggar klyftan mellan den nya klassens metoder och egenskaper och den gamla klassens metoder och egenskaper. Det har samma gränssnitt som den gamla klassen, men inkluderar logik för att mappa gamla metoder till de nya metoderna. Som ett resultat så kan det utföras liknande operationer. Detta liknar det sätt på vilket ett eluttag fungerar som en adapter mellan en amerikansk kontakt och en europeisk kontakt.

Här är ett exempel:

// 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örsta problemet med detta designmönster är att det gör källkoden mer komplex. Du behövde redan underhålla två olika klasser, och nu har du ytterligare en klass – Adapter – att underhålla.

7. Bridge

Med Bridge-mönstret, som är en vidareutveckling av Adapter-mönstret, så får både klassen och klienten separata gränssnitt. Som ett resultat så kan båda fungera även om de har inkompatibla ursprungliga gränssnitt.

Detta hjälper till att utveckla ett mycket löst kopplat gränssnitt mellan de två typerna av objekt. Som ett resultat så förbättras även gränssnittens och deras implementeringars utbyggbarhet för maximal flexibilitet.

Så här kan du använda detta:

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

Du kanske redan har gissat att Bridge-mönstret ökar komplexiteten i kodbasen. Dessutom så slutar de flesta gränssnitt vanligtvis med endast en implementering i verkliga användningsområden. Du drar med andra ord inte så mycket nytta av återanvändbarheten av koden.

8. Composite

Designmönstret Composite hjälper dig att enkelt strukturera och hantera liknande objekt och enheter. Den grundläggande idén bakom Composite-mönstret är att objekten och deras logiska containers kan representeras med hjälp av en enda abstrakt klass (som kan lagra data/metoder som är relaterade till objektet och referenser till sig själv för containern).

Det är mest meningsfullt att använda Composite-mönstret när din datamodell liknar en trädstruktur. Du bör dock inte försöka att förvandla en datamodell utan träd till en trädliknande datamodell bara för att kunna använda Composite-mönstret. Detta kan nämligen ofta ta bort en hel del flexibilitet.

I exemplet nedan så ser du hur du kan använda designmönstret Composite för att konstruera ett förpackningssystem för e-handelsprodukter. Det kan även kan beräkna det totala ordervärdet per paket:

// 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örsta nackdelen med att använda Composite-mönstret är att ändringar av komponenternas gränssnitt kan bli mycket svåra i framtiden. Att utforma gränssnitten tar tid och kraft, och datamodellens trädliknande karaktär kan göra det mycket svårt att göra de ändringar som du vill.

9. Decorator

Decorator-mönstret hjälper dig att lägga till nya funktioner till befintliga objekt. Du sveper helt enkelt in dem i ett nytt objekt. Detta liknar det sätt på vilket du kan slå in en redan inslagen presentförpackning med nytt omslagspapper hur många gånger som du vill: Varje omslag ger dig möjlighet att lägga till så många funktioner som du vill, så det är bra när det gäller flexibilitet.

Ur ett tekniskt perspektiv så är inget arv inblandat. Som ett resultat så får du större frihet när du utformar affärslogik.

I exemplet nedan så ser du hur Decorator-mönstret hjälper dig att lägga till fler funktioner till en standardklass 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()

Nackdelarna med det här mönstret är bland annat att koden är mycket komplex. Det finns nämligen inte något standardmönster för att lägga till nya funktioner med hjälp av Decorator. Det kan hända att du i slutet av din mjukvaruutvecklings-livscykel får många oenhetliga och/eller liknande Decorators.

Om du inte är försiktig när du utformar Decorators så kan det sluta med att du utformar vissa sådana så att de är logiskt beroende av andra. Om detta inte blir löst så kan borttagning eller omstrukturering av Decorators senare i processen orsaka förödelse för stabiliteten i din applikation.

10. Facade

När man bygger de flesta verkliga tillämpningar så blir affärslogiken oftast ganska komplex när man är klar. Det kan hända att flera objekt och metoder är inblandade i utförandet av kärnverksamheten i din applikation. Att hålla reda på deras initialiseringar, beroenden, den korrekta ordningen för utförandet av metoder osv. kan vara ganska knepigt om det inte görs på rätt sätt.

Designmönstret Facade hjälper dig att skapa en abstraktion mellan den miljö som åberopar de ovan nämnda operationerna och de objekt och metoder som är involverade i utförandet av dessa operationer. Denna abstraktion innehåller logiken för initialisering av objekten, spårning av deras beroenden och andra viktiga aktiviteter. Den anropande miljön har ingen information om hur en operation utförs. Som ett resultat så kan du fritt uppdatera logiken utan att göra några brytande ändringar i den anropande klienten.

Så här kan du använda detta 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 nackdel med att använda facade-mönstret är att det lägger till ytterligare ett abstraktionslager mellan affärslogiken och klienten. Det kräver därför ytterligare underhåll. Detta ökar ofta den totala komplexiteten i kodbasen.

Dessutom så blir klassen Facade ett obligatoriskt beroende för att appen ska fungera. Som ett resultat så påverkar eventuella fel i klassen Facade appens funktion direkt.

11. Flyweight

Flyweight-mönstret hjälper dig att lösa problem som involverar objekt med återkommande komponenter på ett minneseffektivt sätt. Det hjälper dig helt enkelt att återanvända de gemensamma komponenterna i din objekt-pool. Som ett resultat så kan du minska belastningen på minnet och även få snabbare exekveringstider.

I exemplet nedan så lagras en stor mening i minnet med hjälp av designmönstret Flyweight. Istället för att lagra varje tecken när det uppstår så identifierar applikationen uppsättningen av distinkta tecken som har använts för att skriva stycket och deras typer (nummer eller alfabet). Den bygger sedan återanvändbara Flyweights för varje tecken som innehåller detaljer om vilket tecken och vilken typ som lagras.

Huvudmatrisen lagrar sedan bara en lista med referenser till dessa flyweights i den ordning som de förekommer i meningen. Då slipper applikationen lagra en instans av teckenobjektet varje gång som det förekommer.

Detta minskar det minne som meningen tar i anspråk med hälften. Tänk på att detta är en mycket grundläggande förklaring av hur textprocessorer lagrar text.

// 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 kanske redan har märkt så ökar Flyweight-mönstret komplexiteten i din programvarudesign genom att det inte är särskilt intuitivt. Om sparande av minne inte är ett akut problem för din app så kan Flyweight-mönstrets extra komplexitet med andra ord göra mer skada än nytta.

Dessutom så byter Flyweight minnet mot bearbetnings-effektivitet. Om du har ont om CPU-cykler så är Flyweight därför ingen bra lösning för dig.

12. Proxy

Proxy-mönstret hjälper dig att ersätta ett objekt med ett annat objekt. Med andra ord så kan proxy-objekt ersätta faktiska objekt (som de är en proxy av) och kontrollera åtkomsten till objektet. Dessa proxy-objekt kan användas för att utföra vissa åtgärder före eller efter att en anrops-begäran skickas vidare till det faktiska objektet.

I exemplet nedan så ser du hur åtkomsten till en databasinstans styrs via en proxy som utför några grundläggande valideringskontroller av begärandena innan de tillåts gå igenom:

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

Det här designmönstret är vanligt förekommande i hela branschen och hjälper till att enkelt genomföra åtgärder före och efter utförandet. Men precis som alla andra designmönster så ökar det även komplexiteten i din kodbas. Undvik därför att använda det om du inte verkligen behöver det.

Du bör även tänka på att ytterligare ett objekt är involverat när du gör anrop till ditt faktiska objekt. Som ett resultat så kan det uppstå en viss latens på grund av de extra bearbetnings-operationerna. Optimering av ditt huvudobjekts prestanda innebär nu även att optimera din proxys metoder för prestanda.

Beteende

Beteendemässiga designmönster hjälper dig att lösa problem kring hur objekt interagerar med varandra. Det kan handla om att dela eller överlämna ansvar/kontroll mellan objekt för att slutföra inställda operationer. Det kan dessutom handla om att överföra/dela data mellan flera objekt på ett så effektivt sätt som möjligt.

13. Chain of Responsibility

Chain of Responsibility-mönstret är ett av de enklaste beteendedesign-mönstren. Det är praktiskt när du utformar logik för operationer som kan hanteras av flera handläggare.

I likhet med hur problemtrappning fungerar i supportteam så skickas kontrollen genom en kedja av handläggare. Den handläggare som ansvarar för att vidta åtgärder slutför operationen. Det här designmönstret används ofta vid utformning av användargränssnitt, där flera lager av komponenter kan hantera en händelse för användarinmatning. Det kan exempelvis handla om en beröring eller en svepning.

Nedan så ser du ett exempel på en upptrappning av ett klagomål med hjälp av mönstret Chain of Responsibility. Klagomålet hanteras av handläggarna på grundval av dess svårighetsgrad:

// 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 uppenbara problemet med det här mönstret är att det är linjärt. Som ett resultat så kan det uppstå en viss latenstid i hanteringen av en operation när ett stort antal handläggare är kedjade till varandra.

Att hålla reda på alla handläggare kan vara en annan svårighet, eftersom det kan bli ganska rörigt efter ett visst antal handläggare. Felsökning är ännu en mardröm eftersom varje begäran kan hamna hos olika handläggare, vilket gör det svårt för dig att standardisera loggnings- och felsökningsprocessen.

14. Iterator

Iterator-mönstret är ganska enkelt och används mycket ofta i nästan alla moderna objektorienterade språk. Du kanske står inför uppgiften att gå igenom en lista med objekt som inte alla är av samma typ. Då kan normala iterations-metoder, som exempelvis for-slingor, bli ganska röriga – särskilt om du även skriver affärslogik inuti dem.

Iterator-mönstret kan hjälpa dig att isolera iterations- och bearbetningslogiken för dina listor från den huvudsakliga affärslogiken.

Här ser du hur du kan använda det på en ganska enkel lista med flera typer av element:

// 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 här mönstret kan vara onödigt komplicerat för listor utan flera typer av element. Om det dessutom finns för många typer av element i en lista så kan det även bli svårt att hantera.

Nyckeln är att identifiera om du verkligen behöver en iterator utifrån din lista och dess framtida ändringsmöjligheter. Dessutom så är Iterator-mönstret endast användbart i listor. Dessa kan ibland begränsa dig till deras linjära åtkomstsätt. Andra datastrukturer kan ibland ge dig större prestanda-fördelar.

15. Mediator

Din applikations-design kan ibland kräva att du leker med ett stort antal olika objekt som rymmer olika typer av affärslogik och som ofta är beroende av varandra. Hanteringen av beroendena kan ibland bli knepig eftersom du måste hålla reda på hur dessa objekt utbyter data och kontrollen mellan dem.

Designmönstret Mediator syftar till att hjälpa dig att lösa detta problem genom att isolera interaktions-logiken för dessa objekt i ett separat objekt i sig självt.

Detta separata objekt kallas mediator och ansvarar för att få arbetet utfört av dina klasser på lägre nivå. Din klient eller den anropande miljön kommer även att interagera med mediator i stället för med de lägre klasserna.

Här är ett exempel på hur designmönstret mediator fungerar:

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

Mediator ger visserligen din appdesign frikoppling och stor flexibilitet. I slutändan så är det dock ytterligare en klass som du måste underhålla. Du måste bedöma om din design verkligen kan dra nytta av en mediator innan du skriver en sådan så att du inte till slut lägger till en onödig komplexitet i din kodbas.

Det är även viktigt att komma ihåg att även om mediator-klassen inte innehåller någon direkt affärslogik så innehåller den fortfarande en hel del kod. Den är avgörande för att din app ska fungera och kan därför snabbt bli ganska komplex.

16. Memento

Versionerings-objekt är ett annat vanligt problem som du möter när du utvecklar appar. Det finns många användningsområden där du behöver upprätthålla ett objekts historik, stödja enkla rollbacks och ibland även stödja återställning av dessa rollbacks. Att skriva logiken för sådana appar kan vara svårt.

Designmönstret Memento är tänkt att lösa detta problem på ett enkelt sätt.

En memento anses vara en ögonblicksbild av ett objekt vid en viss tidpunkt. Designmönstret Memento använder sig av dessa minnesbilder för att bevara ögonblicksbilder av objektet när det ändras över tiden. När du behöver återgå till en gammal version så kan du helt enkelt hämta upp minnet för den.

Så här kan du implementera detta i en textbehandlingsapp:

// 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önstret är en utmärkt lösning för att hantera ett objekts historik, men det kan bli mycket resurskrävande. Varje Memento är nästan en kopia av objektet. Som ett resultat så kan det snabbt svälla upp appens minne om det inte används med måtta.

Med ett stort antal objekt så kan hanteringen av deras livscykel även bli en ganska tråkig uppgift. Utöver allt detta så är klasserna Originator och Caretaker vanligtvis mycket tätt kopplade, vilket ökar komplexiteten i din kodbas.

17. Observer

Observer-mönstret erbjuder en alternativ lösning på problemet med interaktion mellan flera objekt (som tidigare har setts i Mediator-mönstret).

Istället för att låta varje objekt kommunicera med varandra genom en utsedd mediator så gör mönstret Observer att de kan observera varandra. Objekten är utformade för att sända ut händelser när de försöker sända data eller kontroll, och andra objekt som ”lyssnar” på dessa händelser kan sedan ta emot dem och interagera utifrån deras innehåll.

Här är en enkel demonstration av att skicka ut nyhetsbrev till flera personer med hjälp av Observer-mönstret:

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

Observer-mönstret är visserligen ett smidigt sätt att skicka vidare kontroll och data. Det är dock bättre lämpat för situationer där det finns ett stort antal avsändare och mottagare som interagerar med varandra via ett begränsat antal anslutningar. Om objekten skulle ha en-till-en-anslutningar så skulle du förlora den fördel som du får genom att publicera och prenumerera på händelser. Det kommer nämligen alltid bara att finnas en prenumerant för varje publicerare (när det hade varit bättre om det hade hanterats med en direkt kommunikationslinje mellan dem).

Dessutom så kan designmönstret Observer leda till prestandaproblem om prenumerations-händelserna inte hanteras korrekt. Om ett objekt fortsätter att prenumerera på ett annat objekt även när det inte behöver det, så kommer det inte att vara berättigat till garbage collection och kommer att öka minnesförbrukningen i appen.

18. Tillstånd

Designmönstret State är ett av de mest använda designmönstren inom mjukvaruutvecklings-industrin. Populära JavaScript-ramverk som React och Angular förlitar sig i hög grad på State-mönstret för att hantera data och app-beteende som är baserat på dessa data.

Enkelt uttryckt så är State-designmönstret användbart i situationer där du kan definiera definitiva tillstånd för en enhet (som kan vara en komponent, en sida, en app eller en maskin), och enheten har en fördefinierad reaktion på tillstånds-förändringen.

Låt oss säga att du försöker bygga en låneansökans-process. Varje steg i ansökningsprocessen kan definieras som ett tillstånd.

Medan kunden vanligtvis ser en liten lista med förenklade tillstånd för sin ansökan (väntande, under granskning, accepterad och avslagen) så kan det finnas andra steg som är inblandade internt. I varje steg så tilldelas ansökan en särskild person och kan ha unika krav.

Systemet är utformat på ett sådant sätt att när behandlingen av en ansökan är avslutad i ett tillstånd så uppdateras det till nästa tillstånd och nästa relevanta steg påbörjas.

Så här kan du bygga ett system för uppgiftshantering med hjälp av designmönstret 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()

State-mönstret gör visserligen ett bra jobb när det gäller att separera steg i en process. Det kan dock bli extremt svårt att underhålla i stora applikationer som har flera tillstånd.

Din process-design tillåter kanske dessutom mer än bara linjär rörelse genom alla tillstånd. Som ett resultat så får du skriva och underhålla mer kod, eftersom varje övergång till ett tillstånd måste hanteras separat.

19. Strategi

Strategy-mönstret, även känt som Policy-mönstret, syftar till att hjälpa dig att kapsla in och fritt utbyta klasser med hjälp av ett gemensamt gränssnitt. Detta bidrar till att upprätthålla en lös koppling mellan klienten och klasserna och gör att du kan lägga till så många implementeringar som du vill.

Strategy-mönstret är känt för att vara till stor hjälp i situationer där samma operation behövs med hjälp av olika metoder/algoritmer, eller där massiva switchblock måste ersättas med mer människovänlig kod.

Här är ett exempel 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 är utmärkt när det gäller att införa nya varianter av en enhet utan att ändra klienterna särskilt mycket. Det kan dock verka överdrivet om du bara har en handfull variationer att implementera.

Dessutom så tar kapslingen bort finare detaljer om varje variants interna logik. Som ett resultat så är din klient omedveten om hur en variant kommer att bete sig.

20. Visitor

Visitor-mönstret syftar till att hjälpa dig att göra din kod utbyggbar.

Tanken är att tillhandahålla en metod i klassen som gör det möjligt för objekt från andra klasser att enkelt göra ändringar i objekt från den aktuella klassen. De andra objekten besöker det aktuella objektet (även kallat plats-objektet). Det kan även hända att den aktuella klassen accepterar besökarobjekten och plats-objektet hanterar besöket av varje externt objekt på lämpligt sätt.

Så här kan du använda detta:

// 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 enda bristen i den här konstruktionen är att varje besökarklass måste uppdateras när en ny plats läggs till eller ändras. I fall där flera besökare och platsobjekt finns tillsammans så kan detta vara svårt att underhålla.

I övrigt så fungerar metoden utmärkt för att förbättra funktionaliteten hos klasser dynamiskt.

Bästa praxis för att genomföra designmönster

Nu när du har sett de vanligaste designmönstren i JavaScript så kommer här några tips som du bör tänka på när du implementerar dem.

Var särskilt noga med att förstå om ett mönster passar för lösningen

Det här tipset ska tillämpas innan du implementerar ett designmönster i din källkod. Det kan visserligen se ut som att ett designmönster är slutet på alla dina bekymmer. Försök dock att analysera kritiskt om detta är sant.

Det finns många mönster som löser samma problem, men som har olika tillvägagångssätt och olika konsekvenser. Dina kriterier för att välja ett designmönster bör med andra ord inte bara vara om det löser ditt problem eller inte. De bör även ta hänsyn till hur bra det löser ditt problem och om det finns något annat mönster som kan presentera en mer effektiv lösning.

Förstå kostnaderna för att implementera ett mönster innan du börjar

Även om designmönster verkar vara den bästa lösningen på alla tekniska problem så bör du inte hoppa in i implementeringen av dem i din källkod direkt.

När du bedömer konsekvenserna av att implementera en lösning så måste du även ta hänsyn till din egen situation. Har du ett stort team av mjukvaruutvecklare som är skickliga på att förstå och underhålla designmönster? Eller är du en grundare i ett tidigt skede med ett minimalt utvecklingsteam som vill släppa en snabb MVP av din produkt? Om du svarar ja på den sista frågan så är designmönster kanske inte det mest optimala utvecklingssättet för dig.

Designmönster leder inte till en stor återanvändning av kod om de inte planeras i ett mycket tidigt skede av app-designen. Att slumpmässigt använda designmönster i olika skeden kan leda till en onödigt komplex app-arkitektur som du måste ägna veckor åt att förenkla.

Effektiviteten hos ett designmönster kan inte bedömas genom någon form av testning. Det är ditt teams erfarenhet och introspektion som gör att du vet om de fungerar. Om du däremot har tid och resurser att avsätta för dessa aspekter så kan designmönster verkligen lösa dina problem.

Gör inte varje lösning till ett mönster

En annan tumregel att hålla i minnet är att avstå från att försöka göra varje litet problem-lösningspar till ett designmönster. Då kanske du börjar att använda detta överallt.

Det är visserligen bra att identifiera standardlösningar och ha dem i åtanke när du stöter på liknande problem. Men det kan även hända att det nya problemet som du har stött på inte passar in på exakt samma beskrivning som ett äldre problem. I ett sådant fall så kan det sluta med att du implementerar en suboptimal lösning och slösar bort resurser.

Designmönster är idag etablerade som ledande exempel på problem-lösningspar eftersom de har testats av hundratals och tusentals programmerare över tid. Som ett resultat så har de generaliserats så mycket som möjligt. Om du försöker att replikera den ansträngningen genom att bara titta på en massa problem och lösningar och kalla dem likartade så kan det sluta med att du gör mycket mer skada på din kod än vad du någonsin hade förväntat dig.

När bör du använda designmönster?

För att sammanfatta det hela så kommer här några ledtrådar som du bör hålla utkik efter när det gäller designmönster. Alla gäller inte för varje app-utveckling, men de bör ge dig en bra uppfattning om vad du ska titta efter när du funderar på att använda designmönster:

  • Du har ett starkt internt team av utvecklare som förstår designmönster väl.
  • Du följer en SDLC-modell som ger utrymme för djupgående diskussioner om appens arkitektur, och designmönster har tagits upp i dessa diskussioner.
  • Samma uppsättning problem har dykt upp flera gånger i dina diskussioner om design och du vet vilket designmönster som passar för detta.
  • Du har försökt att lösa en mindre variant av ditt problem självständigt med designmönstret.
  • Med designmönstret på plats så ser din kod inte överdrivet komplex ut.

Om ett designmönster löser ditt problem och hjälper dig att skriva en kod som är enkel, återanvändbar, modulär, löst kopplad och tydlig så kan detta vara rätt väg att gå.

Ett annat bra tips att tänka på är att allt inte handlar om designmönster. Designmönster är avsedda att hjälpa dig att lösa problem. De är inte lagar och regler som man måste följa. Faktum är att de ultimata reglerna och lagarna fortfarande är desamma: Håll din kod ren, enkel, läsbar och skalbar. Om ett designmönster hjälper dig att göra detta samtidigt som det löser ditt problem, bör du kunna använda det.

Sammanfattning

JavaScript-designmönster är ett underbart sätt att närma sig problem som flera programmerare har ställts inför under tidens gång. De presenterar beprövade lösningar som strävar efter att hålla din kodbas ren och löst kopplad.

Idag så finns det hundratals tillgängliga designmönster som nästan löser alla problem som du stöter på när du bygger appar. Alla designmönster löser dock inte dina problem varje gång.

Precis som alla andra programmeringskonventioner så är designmönster avsedda att tas som förslag för att lösa problem. De är inte lagar som ska följas hela tiden, och om du behandlar dem som lagar kan det sluta med att du gör stor skada på dina appar.

När din app är klar så behöver du en plats att hysa den på – och Kinsta’s lösningar för applikationshosting är bland de snabbaste och mest tillförlitliga. Du behöver bara logga in på ditt MyKinsta-konto (Kinsta’s anpassade administrativa instrumentpanel), ansluta till ditt GitHub-arkiv och starta! Dessutom så debiteras du endast för de resurser som din app använder.

Vilka designmönster använder du regelbundet i ditt arbete med programvaru-programmering? Finns det ett mönster som vi missade i listan? Låt oss veta detta i kommentarerna nedan!

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.