Bij het bouwen van JavaScript applicaties kun je scenario’s tegenkomen waarbij je objecten op een bepaalde, vooraf gedefinieerde manier moet bouwen, of een gemeenschappelijke klasse moet hergebruiken door hem aan te passen of aan te passen voor meerdere use cases.

Het is natuurlijk niet handig om voor deze problemen steeds opnieuw het wiel uit te vinden.

Dit is waar JavaScript design patterns je te hulp schieten.

JavaScript design patterns bieden je een gestructureerde, herhaalbare manier om veel voorkomende problemen bij de ontwikkeling van JavaScript aan te pakken.

In deze handleiding bekijken we wat JavaScript design patterns zijn en hoe je ze kunt gebruiken in je JavaScript apps.

Wat is een JavaScript design pattern?

JavaScript design patterns zijn herhaalbare template-oplossingen voor veel voorkomende problemen bij de ontwikkeling van JavaScript apps.

Het idee is eenvoudig: programmeurs over de hele wereld werden vaak geconfronteerd met terugkerende problemen bij het ontwikkelen van hun apps. Na verloop van tijd kozen sommige developers ervoor om succesvolle manieren om deze problemen aan te pakken te documenteren, zodat anderen gemakkelijk naar de oplossingen konden teruggrijpen.

Toen steeds meer developers ervoor kozen om deze oplossingen te gebruiken en hun efficiëntie bij het oplossen van hun problemen inzagen, werden ze geaccepteerd als een standaard manier om problemen op te lossen en kregen ze de naam “design patterns”

Naarmate het belang van design patterns beter werd begrepen, werden ze verder ontwikkeld en gestandaardiseerd. De meeste moderne design patterns hebben nu een gedefinieerde structuur, zijn georganiseerd in meerdere categorieën, en worden in informaticagerelateerde opleidingen onderwezen als zelfstandige onderwerpen.

Soorten JavaScript design patterns

Hier zijn enkele van de populairste classificaties van JavaScript design patterns.

Creational

Creational design patterns zijn patterns die helpen bij het oplossen van problemen rond het maken en beheren van nieuwe objectinstanties in JavaScript. Het kan zo eenvoudig zijn als het beperken van een class tot slechts één object of zo complex als het definiëren van een ingewikkelde methode voor het met de hand uitzoeken en toevoegen van elke feature in een JavaScript object.

Enkele voorbeelden van creational design patterns zijn onder andere Singleton, Factory, Abstract Factory en Builder.

Structural

Structural design patterns zijn patterns die problemen helpen oplossen rond het beheer van de structuur (of schema) van JavaScript objecten. Deze problemen kunnen bestaan uit het maken van een relatie tussen twee ongelijke objecten of het abstraheren van sommige eigenschappen van een object voor specifieke gebruikers.

Enkele voorbeelden van structural design patterns zijn Adapter, Bridge, Composite en Facade.

Behavioral

Behavioral design patterns zijn patterns die helpen bij het oplossen van problemen rond de manier waarop controle (en verantwoordelijkheid) wordt overgedragen tussen verschillende objecten. Deze problemen kunnen betrekking hebben op het regelen van de toegang tot een gekoppelde lijst of het opzetten van een enkele entiteit die de toegang tot meerdere soorten objecten kan regelen.

Enkele voorbeelden van behavioral design patterns zijn Command, Iterator, Memento en Observer.

Concurrency

Concurrency design patterns zijn patterns die helpen bij het oplossen van problemen rond multi-threading en multitasking. Deze problemen kunnen bestaan uit het handhaven van een actief object tussen meerdere beschikbare objecten of het afhandelen van meerdere events die aan een systeem worden geleverd door inkomende invoer te demultiplexen en stuk voor stuk af te handelen.

Enkele voorbeelden van concurrency design patterns zijn active object, nuclear react, en scheduler.

Architectural

Architectural design patterns zijn patterns die problemen rond softwareontwerp in brede zin helpen oplossen. Deze hebben meestal betrekking op hoe je een systeem ontwerpt en een hoge beschikbaarheid garandeert, risico’s beperkt, en prestatieproblemen vermijdt.

Twee voorbeelden van architectural design patterns zijn MVC en MVVM.

Elementen van een design pattern

Bijna alle design patterns kunnen worden onderverdeeld in een set van vier belangrijke componenten. Dat zijn:

    • Patternnaam: Deze wordt gebruikt om een design pattern te identificeren tijdens de communicatie met andere gebruikers. Voorbeelden zijn “singleton,” “prototype,” en meer.
    • Probleem: Dit beschrijft het doel van het design pattern. Het is een kleine beschrijving van het probleem dat het design pattern probeert op te lossen. Het kan zelfs een voorbeeldscenario bevatten om het probleem beter uit te leggen. Het kan ook een lijst van voorwaarden bevatten waaraan voldaan moet worden voor een design pattern om het onderliggende probleem volledig op te lossen.
    • Oplossing: Dit is de oplossing voor het onderhavige probleem, opgebouwd uit elementen als classes, methoden, interfaces, enz. Hier ligt het grootste deel van een design pattern – het bevat relaties, verantwoordelijkheden en collaborators van verschillende elementen die duidelijk zijn gedefinieerd.
    • Resultaten: Dit is een analyse van hoe goed het pattern in staat was het probleem op te lossen. Zaken als ruimte- en tijdgebruik worden besproken, samen met alternatieve benaderingen om hetzelfde probleem op te lossen.

Als je meer wilt weten over design patterns en hun ontstaan, dan heeft MSU beknopt studiemateriaal dat je kunt raadplegen.

Waarom zou je design patterns gebruiken?

Er zijn meerdere redenen waarom je design patterns zou willen gebruiken:

  • Ze zijn beproefd en getest: Met een design pattern heb je een beproefde oplossing voor je probleem (zolang het design pattern past bij de beschrijving van je probleem). Je hoeft geen tijd te verspillen aan het zoeken naar alternatieve oplossingen, en je kunt er zeker van zijn dat je een oplossing hebt die de basic prestatieoptimalisatie voor je regelt.
  • Ze zijn gemakkelijk te begrijpen: Design patterns zijn bedoeld om klein, eenvoudig en gemakkelijk te begrijpen te zijn. Je hoeft geen gespecialiseerde programmeur te zijn die al tientallen jaren in een specifieke bedrijfstak werkt om te begrijpen welk design pattern je moet gebruiken. Ze zijn doelbewust generiek (niet beperkt tot een bepaalde programmeertaal) en kunnen worden begrepen door iedereen die over voldoende probleemoplossende vaardigheden beschikt. Dit helpt ook als je een wisseling van handen hebt in je technische team: Een stuk code dat steunt op een design pattern is gemakkelijker te begrijpen voor elke nieuwe softwaredeveloper.
  • Ze zijn eenvoudig te gebruiken: De meeste design patterns zijn heel eenvoudig, zoals je verderop in ons artikel zult zien. Je hoeft niet meerdere programmeerconcepten te kennen om ze in je code te implementeren.
  • Ze stellen een code-architectuur voor die gemakkelijk herbruikbaar is: Herbruikbaarheid en netheid van code worden in de hele tech-industrie sterk aangemoedigd, en design patterns kunnen je helpen dat te bereiken. Omdat deze patterns een standaard manier zijn om problemen op te lossen, hebben hun ontwerpers ervoor gezorgd dat de omvattende apparchitectuur herbruikbaar, flexibel en compatibel blijft met de meeste vormen van code schrijven.
  • Ze besparen tijd en appgrootte: Een van de grootste voordelen van het vertrouwen op een standaard set oplossingen is dat ze je helpen tijd te besparen bij het implementeren ervan. De kans is groot dat je hele devteam design patterns goed kent, zodat het voor hen gemakkelijker wordt om te plannen, te communiceren en samen te werken bij de uitvoering ervan. Beproefde en geteste oplossingen betekenen dat de kans groot is dat je uiteindelijk geen resources kwijtraakt of een omweg maakt bij het bouwen van een feature, waardoor je zowel tijd als ruimte bespaart. Bovendien bieden de meeste programmeertalen je standaard templatebibliotheken die al een aantal veel voorkomende design patterns implementeren, zoals Iterator en Observer.

De top 20 JavaScript design patterns die je wil beheersen

Nu je begrijpt waaruit een design pattern bestaat en waarom je ze nodig hebt, laten we een diepere verder kijken naar hoe enkele van de meest gebruikte JavaScript design patterns kunnen worden geïmplementeerd in een JavaScript app.

Creational

Laten we de discussie beginnen met enkele fundamentele, eenvoudig te leren creational design patterns.

1. Singleton

Het Singleton pattern is een van de meest gebruikte design patterns in de software-ontwikkelingsindustrie. Het probleem dat het wil oplossen is om slechts één instance van een class te beheren. Dit kan van pas komen bij het instantiating van objecten die veel resources vergen, zoals databasehandlers.

Dit is hoe je het kunt implementeren in 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

Hoewel dit het doel goed dient, staat het Singleton pattern erom bekend het debuggen te bemoeilijken, omdat het dependencies maskeert en de toegang tot het initialiseren of vernietigen van instances van een class controleert.

2. Factory

De Factory methode is ook een van de populairste design patterns. Het probleem dat de Factory methode wil oplossen is het maken van objecten zonder de conventionele constructor te gebruiken. In plaats daarvan neemt het de configuratie (of beschrijving) van het object dat je wilt en geeft het nieuw gemaakte object terug.

Dit is hoe je het kunt implementeren in 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
*/

Het Factory design pattern bepaalt hoe de objecten worden gemaakt en biedt je een snelle manier om nieuwe objecten te maken, evenals een uniforme interface die de properties definieert die je objecten zullen hebben. Je kunt zoveel hondenrassen toevoegen als je wilt, maar zolang de methoden en properties die door de rassoorten worden blootgesteld hetzelfde blijven, zullen ze feilloos werken.

Merk echter op dat het Factory pattern vaak kan leiden tot een groot aantal classes die moeilijk te beheren zijn.

3. Abstract Factory

De Abstract Factory methode tilt de Factory methode een niveau hoger door factories abstract en dus vervangbaar te maken zonder dat de callende omgeving de exacte gebruikte factory of zijn interne werking kent. De callende omgeving weet alleen dat alle factories een reeks gemeenschappelijke methoden hebben die hij kan aanroepen om de instantiation actie uit te voeren.

Dit is hoe het geïmplementeerd kan worden met behulp van het vorige voorbeeld:

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

*/

Met het Abstract Factory pattern kun je gemakkelijk concrete factories uitwisselen, en het helpt uniformiteit te bevorderen tussen factories en de gemaakte producten. Het kan echter moeilijk worden om nieuwe soorten producten te introduceren, omdat je dan in meerdere classes wijzigingen moet aanbrengen om nieuwe methoden/properties toe te passen.

4. Builder

Het Builder pattern is een van de meest complexe maar flexibele creatieve JavaScript design patterns. Hiermee kun je elke feature één voor één in je product bouwen, waardoor je volledige controle hebt over hoe je object wordt gebouwd, terwijl je toch de interne details abstraheert.

In het ingewikkelde voorbeeld hieronder zie je het Builder design pattern in actie, samen met Director voor het maken van Pizza’s!

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

Je kunt de Builder koppelen aan een Director, zoals de class PizzaShop in het voorbeeld hierboven, om vooraf een reeks stappen te definiëren die telkens moeten worden gevolgd om een standaardvariant van je product te bouwen, d.w.z. een specifiek recept voor je pizza’s.

Het enige probleem met dit design pattern is dat het vrij complex is om op te zetten en te onderhouden. Het toevoegen van nieuwe mogelijkheden op deze manier is echter eenvoudiger dan met de Factory methode.

5. Prototype

Het Prototype design pattern is een snelle en eenvoudige manier om van bestaande objecten nieuwe objecten te maken door ze te klonen.

Eerst wordt een prototype object gemaakt, dat meerdere keren gekloond kan worden om nieuwe objecten te maken. Het komt van pas als het direct instantiaten van een object een meer resource-intensieve operatie is dan het maken van een kopie van een bestaand object.

In het onderstaande voorbeeld zie je hoe je het Prototype pattern kunt gebruiken om nieuwe documenten te maken op basis van een ingesteld templatedocument:

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

De Prototype methode werkt prima voor gevallen waarin een groot deel van je objecten dezelfde waarden delen, of wanneer het maken van een nieuw object in zijn geheel nogal kostbaar is. Het voelt echter als overkill in gevallen waarin je niet meer dan een paar instances van de class nodig hebt.

Structural

Structural design patterns helpen je je bedrijfslogica te organiseren door bewezen manieren te bieden om je classes te structureren. Er zijn verschillende structurele design patterns die elk tegemoet komen aan unieke gebruikssituaties.

6. Adapter

Een veel voorkomend probleem bij het bouwen van apps is het toestaan van samenwerking tussen incompatibele classes.

Een goed voorbeeld om dit te begrijpen is met behoud van achterwaartse compatibiliteit. Als je een nieuwe versie van een class schrijft, wil je natuurlijk dat die gemakkelijk bruikbaar is op alle plaatsen waar de oude versie werkte. Als je echter brekende wijzigingen aanbrengt, zoals het verwijderen of bijwerken van methoden die cruciaal waren voor het functioneren van de oude versie, kun je met een class komen te zitten die al zijn clients moet bijwerken om uitgevoerd te kunnen worden.

In zulke gevallen kan het Adapter design pattern helpen.

Het Adapter design pattern biedt je een abstractie die de kloof overbrugt tussen de methoden en properties van de nieuwe class en de methoden en properties van de oude class. Het heeft dezelfde interface als de oude class, maar het bevat logica om oude methoden naar de nieuwe methoden te mappen om soortgelijke bewerkingen uit te voeren. Dit is vergelijkbaar met hoe een stopcontact fungeert als adapter tussen een stekker in Amerikaanse stijl en een stekker in Europese stijl.

Hier is een voorbeeld:

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

Het belangrijkste probleem met dit design pattern is dat het complexiteit toevoegt aan je broncode. Je moest al twee verschillende classes onderhouden, en nu heb je nog een class – de Adapter – om te onderhouden.

7. Bridge

Voortbordurend op het Adapter pattern, voorziet het Bridge design pattern zowel de class als de client van aparte interfaces, zodat ze allebei kunnen werken, zelfs in gevallen van incompatibele native interfaces.

Het helpt bij het ontwikkelen van een zeer losjes gekoppelde interface tussen de twee soorten objecten. Dit helpt ook bij het verbeteren van de uitbreidbaarheid van de interfaces en hun implementaties voor maximale flexibiliteit.

Dit is hoe je het kunt gebruiken:

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

Zoals je misschien al geraden hebt, verhoogt het Bridge pattern de complexiteit van de codebase aanzienlijk. Bovendien hebben de meeste interfaces in de praktijk meestal maar één implementatie, dus je profiteert niet echt van de herbruikbaarheid van de code.

8. Composite

Het Composite design pattern helpt je om soortgelijke objecten en entiteiten gemakkelijk te structureren en te beheren. Het basisidee achter het Composite pattern is dat de objecten en hun logische containers kunnen worden voorgesteld met een enkele abstracte class (die gegevens/methoden met betrekking tot het object en verwijzingen naar zichzelf voor de container kan opslaan).

Het is het meest zinvol om het Composite pattern te gebruiken als je gegevensmodel op een boomstructuur lijkt. Je moet echter niet proberen om van een niet-boom-gegevensmodel een boomachtig gegevensmodel te maken alleen maar om het Composite pattern te kunnen gebruiken, want dat kan vaak veel flexibiliteit wegnemen.

In het onderstaande voorbeeld zie je hoe je het Composite design pattern kunt gebruiken om een verpakkingssysteem voor e-commerceproducten te construeren dat ook de totale bestelwaarde per verpakking kan berekenen:

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

Het grootste nadeel van het gebruik van het Composite pattern is dat wijzigingen in de interfaces van de componenten in de toekomst erg uitdagend kunnen zijn. Het ontwerpen van de interfaces kost tijd en moeite, en de boomachtige aard van het gegevensmodel kan het erg moeilijk maken om naar wens wijzigingen aan te brengen.

9. Decorator

Het Decorator pattern helpt je nieuwe mogelijkheden toe te voegen aan bestaande objecten door ze eenvoudigweg in een nieuw object te verpakken. Het is vergelijkbaar met hoe je een al ingepakte cadeauverpakking zo vaak als je wilt kunt inpakken met nieuw inpakpapier: Met elke wikkel kun je zoveel features toevoegen als je wilt, dus het is geweldig voor de flexibiliteit.

Technisch gezien is er geen sprake van inheritance, dus er is meer vrijheid bij het ontwerpen van bedrijfslogica.

In het onderstaande voorbeeld zie je hoe het Decorator pattern helpt om meer mogelijkheden toe te voegen aan een standaard Customer class:

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

De nadelen van dit pattern zijn de hoge complexiteit van de code, omdat er geen standaard pattern is gedefinieerd voor het toevoegen van nieuwe mogelijkheden met behulp van decorators. Aan het eind van de ontwikkelingscyclus van je software kun je met veel niet-uniforme en/of vergelijkbare decorators komen te zitten.

Als je niet voorzichtig bent bij het ontwerpen van de decorators, zou het kunnen gebeuren dat sommige decorators logisch afhankelijk zijn van andere. Als dit niet wordt opgelost, kan het verwijderen of herstructureren van decorators later de stabiliteit van je applicatie in gevaar brengen.

10. Facade

Bij het bouwen van de meeste echte applicaties blijkt de bedrijfslogica meestal behoorlijk complex tegen de tijd dat je klaar bent. Je zou kunnen eindigen met meerdere objecten en methoden die betrokken zijn bij het uitvoeren van kernactiviteiten in je app. Het bijhouden van hun initialisaties, dependencies, de juiste volgorde van methode-uitvoering, enz. kan behoorlijk lastig en foutgevoelig zijn als je het niet goed doet.

Het Facade design pattern helpt je een abstractie te maken tussen de omgeving die de bovengenoemde operaties callt en de objecten en methoden die betrokken zijn bij het voltooien van die operaties. Deze abstractie herbergt de logica voor het initialiseren van de objecten, het bijhouden van hun dependencies, en andere belangrijke activiteiten. De callende omgeving heeft geen informatie over hoe een bewerking wordt uitgevoerd. Je kunt de logica vrijelijk bijwerken zonder de aanroepende client te veranderen.

Dit is hoe je het in een aplicatie kunt gebruiken:

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

Een nadeel van het gebruik van het Facade pattern is dat het een extra laag van abstractie toevoegt tussen je bedrijfslogica en de client, waardoor extra onderhoud nodig is. Meestal verhoogt dit de algehele complexiteit van de codebase.

Bovendien wordt de classFacade een verplichte dependency voor het functioneren van je app – wat betekent dat fouten in de class Facade direct invloed hebben op het functioneren van je app.

11. Flyweight

Het Flyweight pattern helpt je problemen met objecten met repeterende componenten geheugenefficiënt op te lossen door je te helpen de gemeenschappelijke componenten van je objectpool te hergebruiken. Dit helpt de belasting van het geheugen te verminderen en resulteert ook in snellere uitvoeringstijden.

In het onderstaande voorbeeld wordt een grote zin in het geheugen opgeslagen met behulp van het Flyweight design pattern. In plaats van elk karakter op te slaan zoals het voorkomt, identificeert het programma de verzameling van verschillende karakters die gebruikt zijn om de paragraaf te schrijven en hun types (nummer of alfabet) en bouwt herbruikbare flyweights voor elk karakter dat details bevat over welk karakter en type zijn opgeslagen.

De hoofdarray slaat dan gewoon een lijst van verwijzingen naar deze flyweights op in de volgorde waarin ze in de zin voorkomen, in plaats van telkens een instance van het karakterobject op te slaan.

Hierdoor wordt het geheugen van de zin gehalveerd. Bedenk dat dit een zeer elementaire uitleg is van hoe tekstverwerkers tekst opslaan.

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

Zoals je misschien al gemerkt hebt, voegt het Flyweight pattern toe aan de complexiteit van je software-ontwerp doordat het niet bijzonder intuïtief is. Dus, als geheugenbesparing geen urgente zorg is voor je app, kan de toegevoegde complexiteit van Flyweight meer kwaad dan goed doen.

Bovendien ruilen flyweights geheugen voor verwerkingsefficiëntie, dus als je een tekort aan CPU cycli hebt, is Flyweight geen goede oplossing voor je.

12. Proxy

Het Proxy pattern helpt je een object te vervangen door een ander object. Met andere woorden, proxy objecten kunnen de plaats innemen van echte objecten (waar ze een proxy van zijn) en de toegang tot het object controleren. Deze proxy objecten kunnen worden gebruikt om bepaalde acties uit te voeren voordat of nadat een aanroepingsverzoek is doorgegeven aan het eigenlijke object.

In het onderstaande voorbeeld zie je hoe de toegang tot een database-instance wordt geregeld via een proxy die enkele basiscontroles uitvoert op de verzoeken voordat ze worden doorgelaten:

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

Dit design pattern wordt veel gebruikt in de sector en helpt om gemakkelijk operaties voor en na de uitvoering uit te voeren. Maar net als elk ander design pattern voegt het ook complexiteit toe aan je codebase, dus probeer het niet te gebruiken als je het niet echt nodig hebt.

Je moet er ook rekening mee houden dat, omdat er een extra object betrokken is bij het aanroepen van je eigenlijke object, er enige vertraging kan optreden door de toegevoegde verwerkingsoperaties. Het optimaliseren van de prestaties van je hoofdobject betekent nu ook het optimaliseren van de methoden van je proxy voor prestaties.

Behavioral

Behavioral design patterns helpen je problemen op te lossen rond de manier waarop objecten met elkaar interacteren. Dit kan gaan om het delen of doorgeven van verantwoordelijkheid/controle tussen objecten om bepaalde operaties uit te voeren. Het kan ook gaan om het doorgeven/delen van gegevens over meerdere objecten op de meest efficiënte manier.

13. Chain of Responsibility

Het Chain of Responsibility pattern is een van de eenvoudigste behavioral design patterns. Het komt van pas als je logica ontwerpt voor operaties die door meerdere handlers kunnen worden afgehandeld.

Vergelijkbaar met hoe issue escalatie werkt in supportteams, wordt de controle doorgegeven door een keten van handlers, en de handler die verantwoordelijk is voor het nemen van actie voltooit de operatie. Dit design pattern wordt vaak gebruikt in UI ontwerp, waar meerdere lagen van componenten een invoergebeurtenis van de gebruiker kunnen afhandelen, zoals een aanraking of een swipe.

Hieronder zie je een voorbeeld van een klachtescalatie met behulp van het Chain of Responsibility pattern. De klacht wordt behandeld door de handlers op basis van de ernst ervan:

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

Het voor de hand liggende probleem met dit ontwerp is dat het lineair is, dus er kan enige vertraging optreden bij het afhandelen van een operatie als een groot aantal handlers aan elkaar geketend zijn.

Het bijhouden van alle handlers kan een ander pijnpunt zijn, omdat het na een bepaald aantal handlers nogal rommelig kan worden. Debuggen is nog een andere nachtmerrie, omdat elk verzoek kan eindigen bij een andere handler, waardoor het moeilijk wordt om het loggen en debuggen te standaardiseren.

14. Iterator

Het Iterator pattern is heel eenvoudig en wordt veel gebruikt in bijna alle moderne objectgeoriënteerde talen. Als je geconfronteerd wordt met de taak om een lijst van objecten te doorlopen die niet allemaal van hetzelfde type zijn, dan kunnen normale iteratiemethoden, zoals for-lussen, behoorlijk rommelig worden – vooral als je er ook bedrijfslogica in schrijft.

Het Iterator pattern kan je helpen om de iteratie en verwerkingslogica voor je lijsten te isoleren van de belangrijkste bedrijfslogica.

Hier zie je hoe je het kunt gebruiken voor een tamelijk eenvoudige lijst met meerdere soorten elementen:

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

Het is logisch dat dit pattern onnodig complex kan zijn voor lijsten zonder meerdere typen elementen. Als er te veel soorten elementen in een lijst staan, kan het ook moeilijk worden om die te beheren.

De sleutel is om vast te stellen of je echt een iterator nodig hebt op basis van je lijst en de toekomstige veranderingsmogelijkheden ervan. Bovendien is het Iterator pattern alleen nuttig in lijsten, en lijsten kunnen je soms beperken tot hun lineaire wijze van toegang. Andere gegevensstructuren kunnen je soms grotere prestatievoordelen bieden.

15. Mediator

Je applicatieontwerp vereist soms dat je speelt met een groot aantal verschillende objecten die verschillende soorten bedrijfslogica bevatten en vaak van elkaar afhankelijk zijn. Het omgaan met de afhankelijkheden kan soms lastig zijn, omdat je moet bijhouden hoe deze objecten onderling gegevens en besturing uitwisselen.

Het Mediator design pattern is bedoeld om je te helpen dit probleem op te lossen door de interactielogica voor deze objecten te isoleren in een apart object.

Dit aparte object staat bekend als de mediator, en het is verantwoordelijk voor het werk van je lagere classes. Je client of de callende omgeving zal ook interactie hebben met de mediator in plaats van met de classes op lager niveau.

Hier is een voorbeeld van het mediator design pattern in actie:

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

Hoewel de mediator je app-ontwerp voorziet van ontkoppeling en veel flexibiliteit, betekent het wel nog weer een class die je moet onderhouden. Je moet dus beoordelen of je ontwerp daadwerkelijk kan profiteren van een mediator voordat je er een schrijft, zodat je uiteindelijk geen onnodige complexiteit toevoegt aan je codebase.

Het is ook belangrijk om in gedachten te houden dat, ook al bevat de mediator class geen directe bedrijfslogica, ze toch veel code bevat die cruciaal is voor het functioneren van je app en daarom snel behoorlijk complex kan worden.

16. Memento

Versiebeheer van objecten is een ander veel voorkomend probleem waarmee je te maken krijgt bij het ontwikkelen van apps. Er zijn veel use cases waarbij je de geschiedenis van een object moet bijhouden, eenvoudige rollbacks moet ondersteunen, en soms zelfs het terugdraaien van die rollbacks moet ondersteunen. Het schrijven van de logica voor zulke apps kan lastig zijn.

Het Memento design pattern is bedoeld om dit probleem eenvoudig op te lossen.

Een memento wordt beschouwd als een momentopname van een object op een bepaald moment. Het Memento design pattern maakt gebruik van deze memento’s om snapshots van het object te bewaren terwijl het in de loop der tijd wordt gewijzigd. Als je terug moet naar een oude versie, kun je eenvoudig het memento ervoor opvragen.

Hier zie je hoe je het kunt implementeren in een tekstverwerkende app:

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

Hoewel het Memento design pattern een geweldige oplossing is voor het beheren van de geschiedenis van een object, kan het erg veel resources vergen. Omdat elke memento bijna een kopie van het object is, kan het het geheugen van je app snel opblazen als het niet met mate wordt gebruikt.

Met een groot aantal objecten kan het beheer van hun levenscyclus ook een vervelende taak zijn. Bovenop dit alles zijn de Originator en de Caretaker classes gewoonlijk zeer nauw gekoppeld, wat de complexiteit van je codebase nog vergroot.

17. Observer

Het Observer pattern biedt een alternatieve oplossing voor het multi-object-interactie probleem (eerder gezien in het Mediator pattern).

In plaats van elk object met elkaar te laten communiceren via een aangewezen mediator, laat het Observer pattern ze elkaar observeren. Objecten zijn ontworpen om events uit te zenden als ze proberen gegevens of controle te verzenden, en andere objecten die naar deze events “luisteren” kunnen ze dan ontvangen en op basis van de inhoud ervan interacteren.

Hier is een eenvoudige demonstratie van het versturen van nieuwsbrieven naar meerdere mensen via het Observer pattern:

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

Hoewel het Observer pattern een handige manier is om controle en gegevens door te geven, is het beter geschikt voor situaties waarin een groot aantal zenders en ontvangers met elkaar communiceren via een beperkt aantal verbindingen. Als de objecten allemaal een-op-een verbindingen zouden maken, zou je het voordeel verliezen dat je krijgt door te publiceren en te abonneren op events, omdat er altijd maar één abonnee zal zijn voor elke uitgever (terwijl het beter zou zijn geweest als er een directe communicatielijn tussen hen was geweest).

Bovendien kan het Observer design pattern leiden tot prestatieproblemen als de subscription events niet goed worden afgehandeld. Als een object zich blijft abonneren op een ander object, zelfs als dat niet nodig is, komt het niet in aanmerking voor garbage collection en zal het het geheugengebruik van de app verhogen.

18. State

Het State design pattern is een van de meest gebruikte design patterns in de software-devevlopmentsector. Populaire JavaScript frameworks als React en Angular leunen zwaar op het State pattern om gegevens en appgedrag op basis van die gegevens te beheren.

Eenvoudig gezegd is het State design pattern nuttig in situaties waarin je definitieve toestanden van een entiteit kunt definiëren (dat kan een component, een pagina, een app of een machine zijn), en de entiteit heeft een vooraf gedefinieerde reactie op de verandering van de state.

Stel, je probeert een aanvraagprocedure voor een lening op te zetten. Elke stap in het aanvraagproces kan worden gedefinieerd als een state.

Terwijl de klant meestal een kleine lijst ziet met vereenvoudigde states van zijn aanvraag (in behandeling, wordt naar gekeken, geaccepteerd en afgewezen), kunnen er intern nog andere stappen bij betrokken zijn. Bij elk van deze stappen wordt de aanvraag toegewezen aan een aparte persoon en kunnen er unieke eisen aan worden gesteld.

Het systeem is zo ontworpen dat aan het eind van de verwerking in een state de state wordt bijgewerkt tot de volgende in de rij, en de volgende relevante reeks stappen wordt gestart.

Hier zie je hoe je een taakbeheersysteem kunt bouwen met behulp van het State design pattern:

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

Hoewel het State pattern goed werk levert in het scheiden van stappen in een proces, kan het extreem moeilijk te onderhouden worden in grote applicaties die meerdere states hebben.

Bovendien, als je procesontwerp meer toestaat dan het lineair doorlopen van alle toestanden, moet je meer code schrijven en onderhouden, omdat elke toestandsovergang apart behandeld moet worden.

19. Strategy

Ook bekend als het Policy pattern, is het Strategy pattern bedoeld om je te helpen classes te encapsulaten en vrij uit te wisselen met behulp van een gemeenschappelijke interface. Dit zorgt voor een losse koppeling tussen de client en de classes en staat je toe zoveel implementaties toe te voegen als je wilt.

Het Strategy pattern staat erom bekend enorm te helpen in situaties waarin dezelfde operatie nodig is met verschillende methoden/algoritmen, of waarin enorme schakelblokken vervangen moeten worden door meer mensvriendelijke code.

Hier is een voorbeeld van het Strategy pattern:

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

Het Strategy pattern is geweldig als het gaat om het introduceren van nieuwe variaties van een entiteit zonder de clients veel te veranderen. Het kan echter overkill lijken als je maar een handvol variaties te implementeren hebt.

Ook neemt de encapsulation fijnere details weg over de interne logica van elke variant, zodat je client niet weet hoe een variant zich gaat gedragen.

20. Visitor

Het Visitor pattern is bedoeld om je code uitbreidbaar te maken.

Het idee is om een methode in de class aan te bieden waarmee objecten van andere classes gemakkelijk wijzigingen kunnen aanbrengen in objecten van de huidige class. De andere objecten bezoeken het huidige object (ook wel het plaatsobject genoemd), of de huidige class accepteert de bezoekersobjecten, en het plaatsobject handelt het bezoek van elk extern object op de juiste manier af.

Dit is hoe je het kunt gebruiken:

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

De enige tekortkoming van dit ontwerp is dat elke bezoekersclass moet worden bijgewerkt telkens wanneer een nieuwe plaats wordt toegevoegd of gewijzigd. In gevallen waarin meerdere bezoekers en plaatsobjecten samen bestaan, kan dit moeilijk te onderhouden zijn.

Verder werkt de methode prima om de functionaliteit van classes dynamisch uit te breiden.

Beste manieren om design patterns te implementeren

Nu je de meest voorkomende design patterns in JavaScript hebt gezien, zijn hier enkele tips die je in gedachten moet houden bij het implementeren ervan.

Besteed speciale aandacht aan het begrijpen of een pattern bij de oplossing past

Deze tip moet je toepassen voordat je een design pattern implementeert in je broncode. Hoewel het misschien lijkt alsof een design pattern het einde van al je zorgen is, neem toch even de tijd om kritisch te analyseren of dat waar is.

Er zijn veel patterns die hetzelfde probleem oplossen, maar verschillende benaderingen hanteren en verschillende gevolgen hebben. Dus je criteria voor het kiezen van een design pattern moeten niet alleen zijn of het je probleem oplost of niet – het moet ook zijn hoe goed het je probleem oplost en of er een ander pattern is dat een efficiëntere oplossing kan bieden.

Begrijp de kosten van het implementeren van een pattern voordat je begint

Hoewel design patterns de beste oplossing lijken te zijn voor alle engineeringproblemen, moet je ze niet meteen in je broncode gaan implementeren.

Bij het beoordelen van de gevolgen van het implementeren van een oplossing moet je ook rekening houden met je eigen situatie. Heb je een groot team van softwaredevelopers die goed bedreven zijn in het begrijpen en onderhouden van design patterns? Of ben je een beginnende startup met een minimaal ontwikkelteam die snel een MVP van je product wil uitbrengen? Als je de laatste vraag met ja beantwoordt, zijn design patterns misschien niet de meest optimale manier van ontwikkelen voor jou.

Design patterns leiden niet tot zwaar hergebruik van code, tenzij ze in een heel vroeg stadium van het appontwerp worden gepland. Het willekeurig gebruiken van design patterns in verschillende stadia kan leiden tot een onnodig complexe apparchitectuur die je wekenlang zou moeten vereenvoudigen.

De effectiviteit van een design pattern kan door geen enkele vorm van testen worden beoordeeld. Het zijn de ervaring en introspectie van je team die je laten weten of ze werken. Als je de tijd en middelen hebt om aan deze aspecten te besteden, pas dan zullen design patterns je problemen echt oplossen.

Verander niet elke oplossing in een pattern

Een andere vuistregel om in gedachten te houden is om niet te proberen van elk klein probleem een design pattern te maken als oplossing en het te (her)gebruiken wanneer je maar kan.

Hoewel het goed is om standaardoplossingen te identificeren en ze in gedachten te houden als je soortgelijke problemen tegenkomt, is de kans groot dat het nieuwe probleem dat je tegenkwam niet precies dezelfde beschrijving heeft als een ouder probleem. In zo’n geval kun je uiteindelijk een suboptimale oplossing implementeren en resources verspillen.

Design patterns zijn tegenwoordig vastgesteld als toonaangevende voorbeelden van hoe je problemen oplost omdat ze in de loop der tijd door duizenden programmeurs zijn getest en zoveel mogelijk zijn gegeneraliseerd. Als je probeert die inspanning te herhalen door gewoon naar een stel problemen en oplossingen te kijken en ze gelijkaardig te noemen, zou je uiteindelijk veel meer schade aan je code kunnen toebrengen dan je ooit had verwacht.

Wanneer moet je design patterns gebruiken?

Samenvattend zijn hier een paar aanwijzingen waar je op moet letten om design patterns te gebruiken. Ze zijn niet allemaal van toepassing op de ontwikkeling van elke app, maar ze zouden je een goed idee moeten geven van waar je op moet letten als je denkt aan het gebruik van design patterns:

  • Je hebt een sterk intern team van developers dat design patterns goed begrijpt.
  • Je volgt een SDLC model dat ruimte biedt voor diepgaande discussies over de architectuur van je app, en in die discussies zijn design patterns naar voren gekomen.
  • Dezelfde reeks problemen is meerdere keren naar voren gekomen in je ontwerpdiscussies, en je weet welk design pattern daarbij past.
  • Je hebt geprobeerd een kleinere variant van je probleem zelfstandig op te lossen met het design pattern.
  • Met het design pattern op zijn plaats ziet je code er niet overdreven complex uit.

Als een design pattern je probleem oplost en je helpt code te schrijven die eenvoudig, herbruikbaar, modulair, losjes gekoppeld en vrij van “code smell” is, dan is het misschien de juiste weg.

Een andere goede tip om in gedachten te houden is om niet alles over design patterns te laten gaan. Design patterns zijn bedoeld om je te helpen bij het oplossen van problemen. Het zijn geen wetten waar je je aan moet houden of regels die je strikt moet volgen. De uiteindelijke regels en wetten zijn nog steeds dezelfde: Houd je code schoon, eenvoudig, leesbaar en schaalbaar. Als een design pattern je helpt dat te doen en tegelijkertijd je probleem op te lossen, dan moet je er goed mee uit de voeten kunnen.

Samenvatting

JavaScript design patterns zijn een prachtige manier om problemen te benaderen waar meerdere programmeurs in de loop der tijd mee te maken hebben gehad. Ze bieden beproefde oplossingen die ernaar streven je codebase schoon en losjes gekoppeld te houden.

Tegenwoordig zijn er honderden design patterns beschikbaar die bijna elk probleem oplossen dat je tegenkomt bij het bouwen van apps. Maar niet elk design pattern zal echt elke keer je probleem oplossen.

Net als alle andere programmeerconventies zijn design patterns bedoeld als suggesties voor het oplossen van problemen. Het zijn geen wetten die je altijd moet volgen, en als je ze als wetten behandelt, kun je uiteindelijk veel schade aanrichten aan je apps.

Als je app klaar is, heb je een plek nodig om hem te hosten – en Kinsta’s Applicatie Hosting oplossingen behoren tot de snelste, betrouwbaarste en veiligste ter wereld. Je hoeft alleen maar in te loggen op je MyKinsta account (Kinsta’s zelfgebouwde admindashboard), verbinding te maken met je GitHub repository, en je kan aan de slag! Bovendien betaal je alleen voor de resources die je app gebruikt.

Wat zijn de design patterns die jij regelmatig gebruikt bij het programmeren van software? Of is er een pattern dat we in de lijst gemist hebben? Laat het ons weten in de comments hieronder!

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.