Bei der Erstellung von JavaScript-Anwendungen kann es vorkommen, dass du Objekte auf eine bestimmte, vordefinierte Art und Weise erstellen oder eine gemeinsame Klasse wiederverwenden musst, indem du sie für mehrere Anwendungsfälle modifizierst oder anpasst.

Es ist natürlich nicht bequem, diese Probleme immer wieder zu lösen.

Hier kommen die JavaScript Design Patterns zu deiner Rettung.

JavaScript-Entwurfsmuster bieten dir eine strukturierte, wiederholbare Methode, um häufig auftretende Probleme bei der JavaScript-Entwicklung zu lösen.

In diesem Leitfaden werden wir einen Blick darauf werfen, was JavaScript Design Patterns sind und wie du sie in deinen JavaScript-Anwendungen einsetzen kannst.

Was ist ein JavaScript Entwurfsmuster?

JavaScript-Entwurfsmuster sind wiederholbare Musterlösungen für häufig auftretende Probleme bei der Entwicklung von JavaScript-Anwendungen.

Die Idee ist einfach: Programmiererinnen und Programmierer auf der ganzen Welt sind seit den Anfängen der Entwicklung von Apps mit immer wiederkehrenden Problemen konfrontiert. Im Laufe der Zeit haben sich einige Entwickler/innen dazu entschlossen, erprobte Wege zur Lösung dieser Probleme zu dokumentieren, damit andere auf diese Lösungen zurückgreifen können.

Als immer mehr Entwickler diese Lösungen nutzten und ihre Effizienz bei der Lösung ihrer Probleme erkannten, wurden sie als Standardmethode zur Problemlösung akzeptiert und erhielten den Namen „Design Patterns“.

Als die Bedeutung von Entwurfsmustern immer besser verstanden wurde, wurden sie weiterentwickelt und standardisiert. Die meisten modernen Entwurfsmuster haben heute eine definierte Struktur, sind in mehreren Kategorien organisiert und werden in Informatikstudiengängen als eigenständige Themen gelehrt.

Arten von JavaScript Entwurfsmuster

Hier sind einige der gängigsten Klassifizierungen von JavaScript-Entwurfsmustern.

Kreative

Kreative Entwurfsmuster helfen, Probleme bei der Erstellung und Verwaltung neuer Objektinstanzen in JavaScript zu lösen. Das kann so einfach sein wie die Beschränkung einer Klasse auf ein einziges Objekt oder so komplex wie die Definition einer komplizierten Methode zur Auswahl und Hinzufügung jeder Funktion in einem JavaScript-Objekt.

Einige Beispiele für kreative Entwurfsmuster sind Singleton, Factory, Abstract Factory und Builder, um nur einige zu nennen.

Strukturelle

Strukturelle Entwurfsmuster helfen dabei, Probleme bei der Verwaltung der Struktur (oder des Schemas) von JavaScript-Objekten zu lösen. Diese Probleme können darin bestehen, eine Beziehung zwischen zwei ungleichen Objekten herzustellen oder einige Eigenschaften eines Objekts für bestimmte Benutzer zu abstrahieren.

Ein paar Beispiele für strukturelle Entwurfsmuster sind Adapter, Bridge, Composite und Facade.

Verhalten

Verhaltensmuster helfen bei der Lösung von Problemen, bei denen es darum geht, wie die Kontrolle (und Verantwortung) zwischen verschiedenen Objekten weitergegeben wird. Bei diesen Problemen geht es z. B. darum, den Zugriff auf eine verknüpfte Liste zu kontrollieren oder eine einzige Instanz einzurichten, die den Zugriff auf mehrere Arten von Objekten kontrollieren kann.

Einige Beispiele für verhaltensorientierte Entwurfsmuster sind Command, Iterator, Memento und Observer.

Gleichzeitigkeit

Gleichzeitige Entwurfsmuster helfen, Probleme im Zusammenhang mit Multithreading und Multitasking zu lösen. Diese Probleme können darin bestehen, ein aktives Objekt unter mehreren verfügbaren Objekten aufrechtzuerhalten oder mehrere Ereignisse zu verarbeiten, die einem System zugeführt werden, indem eingehende Eingaben demultiplexiert und Stück für Stück verarbeitet werden.

Einige Beispiele für Gleichzeitigkeits-Designmuster sind Active Object, Nuclear React und Scheduler.

Architektonische

Architektonische Entwurfsmuster helfen bei der Lösung von Problemen rund um das Softwaredesign im weitesten Sinne. Sie beziehen sich in der Regel darauf, wie du dein System konzipierst, um eine hohe Verfügbarkeit zu gewährleisten, Risiken zu minimieren und Leistungsengpässe zu vermeiden.

Zwei Beispiele für architektonische Entwurfsmuster sind MVC und MVVM.

Elemente eines Entwurfsmusters

Fast alle Design Pattern lassen sich in vier wichtige Komponenten unterteilen. Sie sind:

  • Name des Musters: Er wird verwendet, um ein Entwurfsmuster bei der Kommunikation mit anderen Benutzern zu identifizieren. Beispiele sind „Singleton“, „Prototyp“ und andere.
  • Problem: Hier wird das Ziel des Entwurfsmusters beschrieben. Es ist eine kurze Beschreibung des Problems, das das Entwurfsmuster zu lösen versucht. Es kann sogar ein Beispielszenario enthalten, um das Problem besser zu erklären. Sie kann auch eine Liste von Bedingungen enthalten, die erfüllt sein müssen, damit ein Entwurfsmuster das Problem vollständig löst.
  • Lösung: Dies ist die Lösung des Problems, die aus Elementen wie Klassen, Methoden, Schnittstellen usw. besteht. Hier liegt der Hauptteil eines Entwurfsmusters – es beinhaltet Beziehungen, Verantwortlichkeiten und Mitwirkende der verschiedenen Elemente, die klar definiert sind.
  • Ergebnisse: Hier wird analysiert, wie gut das Muster das Problem lösen konnte. Dabei werden Aspekte wie Platz- und Zeitbedarf sowie alternative Lösungsansätze für das gleiche Problem diskutiert.

Wenn du mehr über Design Patterns und ihre Entstehung erfahren möchtest, findest du an der MSU kurzes Studienmaterial, auf das du zurückgreifen kannst.

Warum solltest du Entwurfsmuster verwenden?

Es gibt viele Gründe, warum du Design Patterns verwenden solltest:

  • Sie sind erprobt und bewährt: Mit einem Entwurfsmuster hast du eine erprobte Lösung für dein Problem (vorausgesetzt, das Entwurfsmuster passt zur Beschreibung deines Problems). Du musst keine Zeit mit der Suche nach alternativen Lösungen verschwenden und kannst dich darauf verlassen, dass du eine Lösung hast, die die grundlegende Leistungsoptimierung für dich übernimmt.
  • Sie sind leicht zu verstehen: Design Patterns sollen klein, einfach und leicht zu verstehen sein. Du musst kein spezialisierter Programmierer sein, der seit Jahrzehnten in einer bestimmten Branche arbeitet, um zu verstehen, welches Entwurfsmuster du verwenden solltest. Sie sind bewusst allgemein gehalten (nicht auf eine bestimmte Programmiersprache beschränkt) und können von jedem verstanden werden, der über ausreichende Problemlösungsfähigkeiten verfügt. Das ist auch hilfreich, wenn du einen Wechsel in deinem technischen Team hast: Ein Code, der auf einem Entwurfsmuster basiert, ist für jeden neuen Softwareentwickler leichter zu verstehen.
  • Sie sind einfach zu implementieren: Die meisten Entwurfsmuster sind sehr einfach, wie du später in unserem Artikel sehen wirst. Du musst nicht viele Programmierkonzepte kennen, um sie in deinem Code zu implementieren.
  • Sie schlagen eine Code-Architektur vor, die leicht wiederverwendbar ist: Wiederverwendbarkeit und Sauberkeit des Codes werden in der gesamten Tech-Branche stark gefördert, und Design Patterns können dir dabei helfen, dies zu erreichen. Da es sich bei diesen Mustern um eine Standardlösung für Probleme handelt, haben die Designer darauf geachtet, dass die gesamte App-Architektur wiederverwendbar, flexibel und mit den meisten Formen der Codeerstellung kompatibel ist.
  • Sie sparen Zeit und die Größe der App: Einer der größten Vorteile, wenn du dich auf Standardlösungen verlässt, ist, dass du bei der Implementierung Zeit sparen kannst. Die Wahrscheinlichkeit ist groß, dass dein gesamtes Entwicklungsteam die Design Patterns gut kennt, so dass es für sie einfacher ist, bei der Implementierung zu planen, zu kommunizieren und zusammenzuarbeiten. Bei erprobten und getesteten Lösungen ist die Wahrscheinlichkeit groß, dass du bei der Entwicklung einer Funktion keine Ressourcen verschwendest oder einen Umweg gehst, was dir Zeit und Platz spart. Außerdem bieten die meisten Programmiersprachen Standardvorlagenbibliotheken, die bereits einige gängige Entwurfsmuster wie Iterator und Observer implementieren.

Die 20 wichtigsten JavaScript Entwurfsmuster, die du beherrschen solltest

Jetzt, wo du weißt, woraus ein Entwurfsmuster besteht und warum du sie brauchst, wollen wir uns genauer ansehen, wie einige der am häufigsten verwendeten JavaScript-Entwurfsmuster in einer JavaScript-App implementiert werden können.

Kreation

Beginnen wir die Diskussion mit einigen grundlegenden, leicht zu erlernenden kreativen Entwurfsmustern.

1. Singleton

Das Singleton-Muster ist eines der am häufigsten verwendeten Entwurfsmuster in der Softwareentwicklungsbranche. Das Problem, das es lösen soll, ist, dass nur eine einzige Instanz einer Klasse vorhanden ist. Dies kann bei der Instanziierung von ressourcenintensiven Objekten, wie z. B. Datenbank-Handlern, sehr nützlich sein.

Hier erfährst du, wie du es in JavaScript umsetzen kannst:

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

Das Singleton-Muster erfüllt zwar seinen Zweck, ist aber dafür bekannt, dass es die Fehlersuche erschwert, da es Abhängigkeiten verschleiert und den Zugriff auf die Initialisierung oder Zerstörung von Klasseninstanzen kontrolliert.

2. Factory

Die Factory-Methode ist ebenfalls eines der beliebtesten Entwurfsmuster. Das Problem, das mit der Factory-Methode gelöst werden soll, ist die Erstellung von Objekten, ohne den herkömmlichen Konstruktor zu verwenden. Stattdessen nimmt sie die Konfiguration (oder Beschreibung) des gewünschten Objekts auf und gibt das neu erstellte Objekt zurück.

Hier siehst du, wie du sie in JavaScript implementieren kannst:

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

Das Factory-Designmuster steuert, wie die Objekte erstellt werden, und bietet dir eine schnelle Möglichkeit, neue Objekte zu erstellen, sowie eine einheitliche Schnittstelle, die die Eigenschaften deiner Objekte definiert. Du kannst so viele Hunderassen hinzufügen, wie du willst, aber solange die Methoden und Eigenschaften, die die einzelnen Rassen aufweisen, gleich bleiben, werden sie einwandfrei funktionieren.

Beachte jedoch, dass das Factory-Muster oft zu einer großen Anzahl von Klassen führt, die schwer zu verwalten sind.

3. Abstract Factory

Die Abstract Factory-Methode hebt die Factory-Methode auf eine höhere Ebene, indem sie Factories abstrakt und damit austauschbar macht, ohne dass die aufrufende Umgebung die genaue verwendete Factory oder deren interne Funktionsweise kennt. Die aufrufende Umgebung weiß nur, dass alle Factories eine Reihe gemeinsamer Methoden haben, die sie aufrufen kann, um die Instanziierung vorzunehmen.

So kann es mit dem vorherigen Beispiel umgesetzt werden:

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

*/

Das Muster der abstract Factories erleichtert den Austausch konkreter Factories und fördert die Einheitlichkeit der Fabriken und der erzeugten Produkte. Allerdings kann es schwierig werden, neue Arten von Produkten einzuführen, da du in mehreren Klassen Änderungen vornehmen musst, um neue Methoden/Eigenschaften unterzubringen.

4. Builder

Das Builder-Muster ist eines der komplexesten und gleichzeitig flexibelsten JavaScript-Designmuster. Es ermöglicht es dir, jedes Feature einzeln in dein Produkt einzubauen. So hast du die volle Kontrolle über den Aufbau deines Objekts und kannst gleichzeitig von den internen Details abstrahieren.

In dem komplizierten Beispiel unten siehst du das Builder Design Pattern in Aktion zusammen mit Director, um Pizzas zu machen!

// 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 kannst den Builder mit einem Director koppeln, wie die Klasse PizzaShop im obigen Beispiel zeigt, um eine Reihe von Schritten vorzugeben, die du jedes Mal befolgen musst, um eine Standardvariante deines Produkts zu erstellen, z. B. ein bestimmtes Rezept für deine Pizzen.

Der einzige Nachteil dieses Entwurfsmusters ist, dass es ziemlich komplex in der Einrichtung und Pflege ist. Das Hinzufügen neuer Funktionen ist auf diese Weise jedoch einfacher als die Factory-Methode.

5. Prototyp

Das Entwurfsmuster Prototype ist eine schnelle und einfache Methode, um neue Objekte aus bestehenden Objekten zu erstellen, indem man sie klont.

Zunächst wird ein Prototyp-Objekt erstellt, das mehrfach geklont werden kann, um neue Objekte zu erstellen. Es ist praktisch, wenn die direkte Instanziierung eines Objekts ressourcenintensiver ist als die Erstellung einer Kopie eines bestehenden Objekts.

Im folgenden Beispiel siehst du, wie du das Prototyp-Muster verwenden kannst, um neue Dokumente auf der Grundlage einer vorgegebenen Dokumentvorlage zu erstellen:

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

Die Prototyp-Methode eignet sich gut für Fälle, in denen ein großer Teil deiner Objekte die gleichen Werte hat oder wenn die Erstellung eines neuen Objekts sehr kostspielig ist. In Fällen, in denen du nicht mehr als ein paar Instanzen der Klasse brauchst, ist sie jedoch zu aufwendig.

Strukturelle

Strukturelle Entwurfsmuster helfen dir dabei, deine Geschäftslogik zu organisieren, indem sie bewährte Wege zur Strukturierung deiner Klassen aufzeigen. Es gibt eine Vielzahl von strukturellen Entwurfsmustern, die jeweils auf bestimmte Anwendungsfälle zugeschnitten sind.

6. Adapter

Ein häufiges Problem bei der Entwicklung von Anwendungen ist die Zusammenarbeit zwischen inkompatiblen Klassen.

Ein gutes Beispiel, um das zu verstehen, ist die Aufrechterhaltung der Abwärtskompatibilität. Wenn du eine neue Version einer Klasse schreibst, möchtest du natürlich, dass sie an allen Stellen, an denen die alte Version funktionierte, problemlos verwendet werden kann. Wenn du jedoch Änderungen vornimmst, wie z. B. das Entfernen oder Aktualisieren von Methoden, die für das Funktionieren der alten Version entscheidend waren, kann es passieren, dass du eine Klasse bekommst, deren Clients alle aktualisiert werden müssen, um sie ausführen zu können.

In solchen Fällen kann das Adapter Design Pattern helfen.

Das Adapter-Entwurfsmuster bietet dir eine Abstraktion, die die Kluft zwischen den Methoden und Eigenschaften der neuen Klasse und den Methoden und Eigenschaften der alten Klasse überbrückt. Sie hat dieselbe Schnittstelle wie die alte Klasse, enthält aber eine Logik, mit der die alten Methoden auf die neuen Methoden abgebildet werden, um ähnliche Vorgänge auszuführen. Dies ist vergleichbar mit einer Steckdose, die als Adapter zwischen einem US-amerikanischen und einem europäischen Stecker fungiert.

Hier ist ein Beispiel:

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

Das Hauptproblem bei diesem Entwurfsmuster ist, dass es die Komplexität deines Quellcodes erhöht. Du musstest bereits zwei verschiedene Klassen pflegen, und jetzt kommt noch eine weitere Klasse hinzu – der Adapter.

7. Bridge

Als Erweiterung des Adapter-Musters bietet das Bridge-Muster sowohl der Klasse als auch dem Client getrennte Schnittstellen, so dass beide auch bei inkompatiblen nativen Schnittstellen funktionieren können.

Es hilft bei der Entwicklung einer sehr lose gekoppelten Schnittstelle zwischen den beiden Objekttypen. Dies trägt auch dazu bei, die Erweiterbarkeit der Schnittstellen und ihrer Implementierungen zu verbessern, um maximale Flexibilität zu erreichen.

Hier erfährst du, wie du sie nutzen kannst:

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

Wie du vielleicht schon vermutet hast, erhöht das Bridge-Muster die Komplexität der Codebasis erheblich. Außerdem gibt es für die meisten Schnittstellen in der Praxis nur eine einzige Implementierung, so dass du nicht wirklich von der Wiederverwendbarkeit des Codes profitierst.

8. Composite

Das Composite Design Pattern hilft dir, ähnliche Objekte und Entitäten einfach zu strukturieren und zu verwalten. Die Grundidee hinter dem Composite-Muster ist, dass die Objekte und ihre logischen Container durch eine einzige abstrakte Klasse dargestellt werden können (die Daten/Methoden für das Objekt und Referenzen auf sich selbst für den Container speichern kann).

Die Verwendung des Composite-Musters ist am sinnvollsten, wenn dein Datenmodell einer Baumstruktur ähnelt. Du solltest jedoch nicht versuchen, ein Datenmodell ohne Baumstruktur in ein baumähnliches Datenmodell umzuwandeln, nur um das Composite-Muster zu verwenden, denn dadurch geht oft eine Menge Flexibilität verloren.

Im folgenden Beispiel siehst du, wie du das Composite Design Pattern nutzen kannst, um ein Verpackungssystem für E-Commerce-Produkte zu erstellen, das auch den Gesamtbestellwert pro Paket berechnen kann:

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

Der größte Nachteil bei der Verwendung des Composite-Musters ist, dass Änderungen an den Komponentenschnittstellen in der Zukunft sehr schwierig sein können. Die Entwicklung der Schnittstellen ist zeitaufwändig und mühsam, und die baumartige Struktur des Datenmodells kann es sehr schwierig machen, Änderungen nach Belieben vorzunehmen.

9. Decorator

Das Decorator-Muster hilft dir, neue Funktionen zu bestehenden Objekten hinzuzufügen, indem du sie einfach in ein neues Objekt verpackst. Das ist so ähnlich, wie wenn du einen bereits eingepackten Geschenkkarton beliebig oft mit neuem Geschenkpapier umwickeln könntest: Mit jeder Umhüllung kannst du so viele Funktionen hinzufügen, wie du möchtest – die Flexibilität ist also groß.

Aus technischer Sicht gibt es keine Vererbung, sodass du bei der Entwicklung der Geschäftslogik mehr Freiheit hast.

Im folgenden Beispiel siehst du, wie das Decorator-Muster dabei hilft, einer Standardklasse Customer mehr Funktionen hinzuzufügen:

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

Zu den Nachteilen dieses Musters gehört die hohe Komplexität des Codes, da es kein Standardmuster für das Hinzufügen neuer Funktionen mithilfe von Decorators gibt. Es kann sein, dass du am Ende des Lebenszyklus deiner Softwareentwicklung eine Menge uneinheitlicher und/oder ähnlicher Dekoratoren hast.

Wenn du beim Design der Dekoratoren nicht aufpasst, kann es passieren, dass einige Dekoratoren logisch von anderen abhängig sind. Wenn dies nicht behoben wird, kann das Entfernen oder Umstrukturieren von Dekoratoren zu einem späteren Zeitpunkt die Stabilität deiner Anwendung beeinträchtigen.

10. Facade

Wenn du die meisten realen Anwendungen entwickelst, wird die Geschäftslogik meist ziemlich komplex, wenn du fertig bist. Es kann vorkommen, dass du mehrere Objekte und Methoden hast, die an der Ausführung von Kernoperationen in deiner Anwendung beteiligt sind. Die Übersicht über ihre Initialisierungen, Abhängigkeiten, die richtige Reihenfolge der Methodenausführung usw. zu behalten, kann ziemlich kompliziert und fehleranfällig sein, wenn du es nicht richtig machst.

Das Facade Design Pattern hilft dir, eine Abstraktion zwischen der Umgebung, die die oben genannten Vorgänge aufruft, und den Objekten und Methoden zu schaffen, die an der Ausführung dieser Vorgänge beteiligt sind. Diese Abstraktion enthält die Logik für die Initialisierung der Objekte, die Verfolgung ihrer Abhängigkeiten und andere wichtige Aktivitäten. Die aufrufende Umgebung hat keine Informationen darüber, wie eine Operation ausgeführt wird. Du kannst die Logik frei aktualisieren, ohne den aufrufenden Client zu verändern.

So kannst du sie in einer Anwendung verwenden:

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

Ein Nachteil des Facade Patterns ist, dass es eine zusätzliche Abstraktionsebene zwischen deiner Geschäftslogik und dem Client einfügt, was zusätzliche Wartung erfordert. In den meisten Fällen erhöht sich dadurch die Gesamtkomplexität der Codebasis.

Außerdem wird die Klasse Facade zu einer zwingenden Abhängigkeit für das Funktionieren deiner App. Das bedeutet, dass sich Fehler in der Klasse Facade direkt auf das Funktionieren deiner App auswirken.

11. Flyweight

Das Flyweight-Muster hilft dir, Probleme mit Objekten mit sich wiederholenden Komponenten auf speichereffiziente Weise zu lösen, indem es dir hilft, die gemeinsamen Komponenten deines Objektpools wiederzuverwenden. Dadurch wird der Speicher entlastet und die Ausführungszeit verkürzt.

Im folgenden Beispiel wird ein großer Satz mit Hilfe des Flyweight-Designmusters im Speicher abgelegt. Anstatt jedes einzelne Zeichen zu speichern, identifiziert das Programm die Menge der verschiedenen Zeichen, die zum Schreiben des Absatzes verwendet wurden, sowie deren Typen (Zahl oder Alphabet) und erstellt für jedes Zeichen wiederverwendbare Flyweights, die Angaben darüber enthalten, welches Zeichen und welcher Typ gespeichert sind.

Das Hauptarray speichert dann einfach eine Liste von Verweisen auf diese Fliegengewichte in der Reihenfolge, in der sie im Satz vorkommen, anstatt eine Instanz des Zeichenobjekts zu speichern, sobald es auftritt.

Dadurch wird der Speicherbedarf des Satzes um die Hälfte reduziert. Bedenke, dass dies eine sehr grundlegende Erklärung dafür ist, wie Textverarbeitungsprogramme Text speichern.

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

Wie du vielleicht schon gemerkt hast, macht das Flyweight-Muster dein Softwaredesign komplizierter, weil es nicht besonders intuitiv ist. Wenn es für deine Anwendung also nicht so wichtig ist, Speicherplatz zu sparen, kann die zusätzliche Komplexität von Flyweight mehr schaden als nützen.

Außerdem tauschen Flyweights Speicher gegen Verarbeitungseffizienz. Wenn du also wenig CPU-Zyklen zur Verfügung hast, ist Flyweight keine gute Lösung für dich.

12. Proxy

Mit dem Proxy-Muster kannst du ein Objekt durch ein anderes Objekt ersetzen. Mit anderen Worten: Proxy-Objekte können an die Stelle von Objekten treten, die sie eigentlich sind, und den Zugriff auf das Objekt kontrollieren. Diese Proxy-Objekte können verwendet werden, um bestimmte Aktionen auszuführen, bevor oder nachdem eine Aufrufanforderung an das eigentliche Objekt weitergeleitet wird.

Im folgenden Beispiel siehst du, wie der Zugriff auf eine Datenbankinstanz über einen Proxy gesteuert wird, der einige grundlegende Überprüfungen der Anfragen durchführt, bevor er sie durchlässt:

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

Dieses Entwurfsmuster wird in der Branche häufig verwendet und hilft dabei, Vorgänge vor und nach der Ausführung einfach zu implementieren. Wie jedes andere Entwurfsmuster erhöht es jedoch auch die Komplexität deiner Codebasis, daher solltest du es nicht verwenden, wenn du es nicht wirklich brauchst.

Außerdem solltest du bedenken, dass ein zusätzliches Objekt an den Aufrufen deines eigentlichen Objekts beteiligt ist, so dass es aufgrund der zusätzlichen Verarbeitungsvorgänge zu einer gewissen Latenz kommen kann. Wenn du die Leistung deines Hauptobjekts optimierst, musst du auch die Methoden deines Proxys auf Leistung optimieren.

Verhalten

Verhaltensmuster helfen dir, Probleme zu lösen, bei denen es darum geht, wie Objekte miteinander interagieren. Dazu kann die gemeinsame Nutzung oder die Übergabe von Verantwortung/Kontrolle zwischen Objekten gehören, um bestimmte Operationen durchzuführen. Es kann auch darum gehen, Daten so effizient wie möglich an mehrere Objekte weiterzugeben oder gemeinsam zu nutzen.

13. Chain of Responsibility

Das Chain of Responsibility Muster ist eines der einfachsten Verhaltensmuster. Es ist nützlich, wenn du eine Logik für Operationen entwickelst, die von mehreren Handlern ausgeführt werden können.

Ähnlich wie bei der Problemeskalation in Support-Teams wird die Kontrolle über eine Kette von Handlern weitergegeben, und der Handler, der für die Aktion verantwortlich ist, schließt die Operation ab. Dieses Entwurfsmuster wird häufig bei der Gestaltung von Benutzeroberflächen verwendet, wo mehrere Komponentenschichten ein Eingabeereignis des Benutzers verarbeiten können, z. B. eine Berührung oder ein Wischen.

Unten siehst du ein Beispiel für eine Beschwerde-Eskalation, bei der das Chain of Responsibility-Muster verwendet wird. Die Beschwerde wird von den Bearbeitern je nach Schweregrad bearbeitet:

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

Der offensichtliche Nachteil dieses Musters ist, dass es linear ist, d.h. es kann zu einer gewissen Latenz bei der Bearbeitung eines Vorgangs kommen, wenn eine große Anzahl von Bearbeitern miteinander verkettet ist.

Die Übersicht über alle Handler zu behalten, kann ein weiteres Problem sein, da es ab einer bestimmten Anzahl von Handlern ziemlich unübersichtlich werden kann. Die Fehlersuche ist ein weiterer Albtraum, da jede Anfrage bei einem anderen Handler enden kann, was es schwierig macht, den Logging- und Debugging-Prozess zu standardisieren.

14. Iterator

Das Iterator-Muster ist recht einfach und wird in fast allen modernen objektorientierten Sprachen häufig verwendet. Wenn du eine Liste von Objekten durchgehen musst, die nicht alle vom gleichen Typ sind, können normale Iterationsmethoden, wie z. B. for-Schleifen, ziemlich unübersichtlich werden – vor allem, wenn du darin auch Geschäftslogik schreibst.

Das Iterator-Muster kann dir helfen, die Iterations- und Verarbeitungslogik für deine Listen von der Hauptgeschäftslogik zu isolieren.

Im Folgenden zeige ich dir, wie du es bei einer einfachen Liste mit mehreren Arten von Elementen einsetzen kannst:

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

Es versteht sich von selbst, dass dieses Muster für Listen ohne mehrere Elementtypen unnötig komplex sein kann. Wenn eine Liste zu viele Arten von Elementen enthält, kann sie außerdem schwer zu verwalten sein.

Entscheidend ist, dass du anhand deiner Liste und ihrer zukünftigen Änderungsmöglichkeiten feststellst, ob du wirklich einen Iterator brauchst. Außerdem ist das Iterator-Muster nur bei Listen nützlich, und Listen können dich manchmal auf ihren linearen Zugriffsmodus beschränken. Andere Datenstrukturen können dir manchmal größere Leistungsvorteile bieten.

15. Mediator

Bei der Entwicklung deiner Anwendung kann es vorkommen, dass du mit einer großen Anzahl verschiedener Objekte arbeiten musst, die verschiedene Arten von Geschäftslogik enthalten und oft voneinander abhängen. Der Umgang mit diesen Abhängigkeiten kann manchmal schwierig sein, da du den Überblick darüber behalten musst, wie diese Objekte Daten und Kontrolle untereinander austauschen.

Das Mediator Design Pattern soll dir helfen, dieses Problem zu lösen, indem es die Interaktionslogik für diese Objekte in einem eigenen Objekt isoliert.

Dieses separate Objekt wird als Mediator bezeichnet und ist dafür verantwortlich, dass deine untergeordneten Klassen ihre Arbeit erledigen. Dein Client oder die aufrufende Umgebung interagiert ebenfalls mit dem Mediator und nicht mit den untergeordneten Klassen.

Hier ist ein Beispiel für das Mediator-Designmuster in Aktion:

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

Der Mediator bietet deinem App-Design zwar Entkopplung und eine große Flexibilität, aber letztendlich ist er eine weitere Klasse, die du pflegen musst. Bevor du einen Mediator schreibst, musst du prüfen, ob dein Design wirklich von einem Mediator profitieren kann, damit du deine Codebasis nicht unnötig komplex machst.

Du musst auch bedenken, dass die Mediatorenklasse zwar keine direkte Geschäftslogik enthält, aber dennoch eine Menge Code, der für das Funktionieren deiner App wichtig ist und daher schnell ziemlich komplex werden kann.

16. Memento

Die Versionierung von Objekten ist ein weiteres häufiges Problem, mit dem du bei der Entwicklung von Apps konfrontiert wirst. Es gibt viele Anwendungsfälle, in denen du die Historie eines Objekts aufrechterhalten, einfache Rollbacks unterstützen und manchmal sogar die Rückgängigmachung dieser Rollbacks ermöglichen musst. Die Logik für solche Anwendungen zu schreiben, kann schwierig sein.

Das Memento Design Pattern ist dafür gedacht, dieses Problem einfach zu lösen.

Ein Memento ist ein Schnappschuss eines Objekts zu einem bestimmten Zeitpunkt. Das Memento-Entwurfsmuster nutzt diese Mementos, um Schnappschüsse des Objekts zu erhalten, wenn es im Laufe der Zeit verändert wird. Wenn du zu einer alten Version zurückkehren musst, kannst du einfach das Memento für diese Version abrufen.

Hier siehst du, wie du es in einer Textverarbeitungs-App implementieren kannst:

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

Das Memento-Entwurfsmuster ist zwar eine großartige Lösung, um die Historie eines Objekts zu verwalten, aber es kann sehr ressourcenintensiv werden. Da jedes Memento fast eine Kopie des Objekts ist, kann es den Speicher deiner App sehr schnell aufblähen, wenn es nicht in Maßen verwendet wird.

Bei einer großen Anzahl von Objekten kann auch die Verwaltung ihres Lebenszyklus eine ziemlich mühsame Aufgabe sein. Darüber hinaus sind die Klassen Originator und Caretaker in der Regel sehr eng miteinander verbunden, was die Komplexität deiner Codebasis noch erhöht.

17. Observer

Das Observer-Muster bietet eine alternative Lösung für das Problem der Interaktion zwischen mehreren Objekten (wie es bereits beim Mediator-Muster der Fall war).

Anstatt jedes Objekt über einen bestimmten Mediator miteinander kommunizieren zu lassen, erlaubt das Observer-Muster, dass sich die Objekte gegenseitig beobachten. Die Objekte senden Ereignisse aus, wenn sie versuchen, Daten oder Steuerungsinformationen zu übermitteln. Andere Objekte, die auf diese Ereignisse „hören“, können sie dann empfangen und auf der Grundlage ihres Inhalts interagieren.

Hier ist eine einfache Demonstration des Versendens von Newslettern an mehrere Personen mithilfe des Observer-Musters:

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

Das Observer-Muster ist zwar eine elegante Methode, um Kontrolle und Daten weiterzugeben, aber es eignet sich besser für Situationen, in denen eine große Anzahl von Sendern und Empfängern über eine begrenzte Anzahl von Verbindungen miteinander interagieren. Wenn die Objekte alle eine Eins-zu-Eins-Verbindung herstellen würden, würdest du den Vorteil verlieren, den du durch das Veröffentlichen und Abonnieren von Ereignissen erhältst, da es immer nur einen Abonnenten für jeden Verleger gibt (während eine direkte Kommunikation zwischen ihnen besser wäre).

Außerdem kann das Observer-Designmuster zu Leistungsproblemen führen, wenn die Abonnement-Ereignisse nicht richtig behandelt werden. Wenn ein Objekt weiterhin ein anderes Objekt abonniert, auch wenn es das nicht muss, kommt es nicht für die Garbage Collection in Frage und erhöht den Speicherverbrauch der App.

18. State

Das State Design Pattern ist eines der am häufigsten verwendeten Design Patterns in der Softwareentwicklungsbranche. Beliebte JavaScript-Frameworks wie React und Angular nutzen das State-Pattern, um Daten und das Verhalten der App auf Basis dieser Daten zu verwalten.

Vereinfacht gesagt, ist das State Design Pattern hilfreich, wenn du definitive Zustände einer Entität (z. B. einer Komponente, einer Seite, einer App oder einer Maschine) definieren kannst und die Entität eine vordefinierte Reaktion auf die Zustandsänderung hat.

Nehmen wir an, du versuchst, einen Kreditantragsprozess zu erstellen. Jeder Schritt im Antragsprozess kann als ein Zustand definiert werden.

Während der Kunde in der Regel eine kleine Liste mit vereinfachten Zuständen seines Antrags sieht (ausstehend, in Prüfung, angenommen und abgelehnt), können intern noch weitere Schritte hinzukommen. Bei jedem dieser Schritte wird der Antrag einer bestimmten Person zugewiesen und kann einzigartige Anforderungen haben.

Das System ist so konzipiert, dass am Ende der Bearbeitung in einem Status der Status auf den nächsten in der Reihe aktualisiert wird und der nächste relevante Satz von Schritten gestartet wird.

Hier erfährst du, wie du ein Aufgabenverwaltungssystem mit dem State Design Pattern aufbauen kannst:

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

Das State-Pattern eignet sich zwar hervorragend, um die einzelnen Schritte eines Prozesses voneinander zu trennen, aber bei großen Anwendungen mit mehreren Zuständen kann es extrem schwierig werden, es zu pflegen.

Außerdem musst du mehr Code schreiben und pflegen, wenn dein Prozessdesign mehr als nur eine lineare Bewegung durch alle Zustände zulässt, da jeder Zustandsübergang separat behandelt werden muss.

19. Strategy

Das Strategy-Pattern, auch bekannt als Policy-Pattern, soll dir dabei helfen, Klassen zu kapseln und frei auszutauschen, indem eine gemeinsame Schnittstelle verwendet wird. Dadurch wird eine lose Kopplung zwischen dem Client und den Klassen aufrechterhalten und du kannst so viele Implementierungen hinzufügen, wie du möchtest.

Das Strategy-Pattern ist dafür bekannt, dass es in Situationen, in denen derselbe Vorgang mit verschiedenen Methoden/Algorithmen ausgeführt werden muss oder in denen umfangreiche Schaltblöcke durch menschenfreundlicheren Code ersetzt werden müssen, eine große Hilfe ist.

Hier ist ein Beispiel für das Strategy-Muster:

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

Das Strategy-Muster eignet sich hervorragend, wenn es darum geht, neue Variationen einer Entität einzuführen, ohne die Clients stark zu verändern. Es kann aber zu viel sein, wenn du nur eine Handvoll Variationen implementieren musst.

Außerdem werden durch die Kapselung feinere Details über die interne Logik der einzelnen Varianten entfernt, so dass dein Kunde nicht weiß, wie sich eine Variante verhalten wird.

20. Visitor

Das Visitor-Muster soll dir helfen, deinen Code erweiterbar zu machen.

Die Idee ist, eine Methode in der Klasse bereitzustellen, die es Objekten anderer Klassen ermöglicht, auf einfache Weise Änderungen an Objekten der aktuellen Klasse vorzunehmen. Die anderen Objekte besuchen das aktuelle Objekt (auch Platzobjekt genannt), oder die aktuelle Klasse akzeptiert die Besucherobjekte, und das Platzobjekt behandelt den Besuch jedes externen Objekts entsprechend.

So kannst du es verwenden:

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

Der einzige Nachteil dieses Designs ist, dass jede Besucherklasse aktualisiert werden muss, wenn ein neuer Ort hinzugefügt oder geändert wird. In Fällen, in denen mehrere Besucher- und Ortsobjekte zusammen existieren, kann dies schwierig zu pflegen sein.

Abgesehen davon eignet sich die Methode hervorragend, um die Funktionalität von Klassen dynamisch zu erweitern.

Best Practices für die Umsetzung von Entwurfsmuster

Nachdem du nun die gängigsten Design Patterns in JavaScript kennengelernt hast, findest du hier einige Tipps, die du bei ihrer Umsetzung beachten solltest.

Achte besonders darauf, ob ein Muster zur Lösung passt

Diesen Tipp solltest du beherzigen, bevor du ein Entwurfsmuster in deinen Quellcode einbaust. Auch wenn es so aussieht, als wäre ein Entwurfsmuster das Ende all deiner Sorgen, nimm dir einen Moment Zeit, um kritisch zu analysieren, ob das auch stimmt.

Es gibt viele Muster, die das gleiche Problem lösen, aber unterschiedliche Ansätze verfolgen und unterschiedliche Konsequenzen haben. Deine Kriterien für die Auswahl eines Entwurfsmusters sollten also nicht nur sein, ob es dein Problem löst oder nicht – sie sollten auch sein, wie gut es dein Problem löst und ob es ein anderes Muster gibt, das eine effizientere Lösung darstellt.

Verstehe die Kosten für die Implementierung eines Musters, bevor du anfängst

Auch wenn Entwurfsmuster die beste Lösung für alle technischen Probleme zu sein scheinen, solltest du sie nicht sofort in deinen Quellcode implementieren.

Wenn du die Folgen der Implementierung einer Lösung abschätzt, musst du auch deine eigene Situation berücksichtigen. Hast du ein großes Team von Softwareentwicklern, die Design Patterns gut verstehen und pflegen können? Oder bist du ein Gründer in der Anfangsphase mit einem kleinen Entwicklungsteam, das schnell eine MVP-Version deines Produkts herausbringen will? Wenn du die letzte Frage mit Ja beantwortest, sind Design Patterns vielleicht nicht die optimale Entwicklungsmethode für dich.

Design Patterns führen nicht zu einer hohen Wiederverwendung von Code, es sei denn, sie werden bereits in einer sehr frühen Phase des App-Designs geplant. Die wahllose Verwendung von Entwurfsmustern in verschiedenen Phasen kann zu einer unnötig komplexen App-Architektur führen, für deren Vereinfachung du Wochen brauchen würdest.

Die Effektivität eines Entwurfsmusters lässt sich nicht durch irgendwelche Tests beurteilen. Nur die Erfahrung deines Teams und die Selbstbeobachtung werden dir zeigen, ob sie funktionieren. Nur wenn du die Zeit und die Ressourcen hast, um diese Aspekte zu berücksichtigen, werden Design Patterns deine Probleme wirklich lösen.

Nicht jede Lösung in ein Muster verwandeln

Eine weitere Faustregel, die du beachten solltest, ist, nicht zu versuchen, jedes kleine Problem-Lösungs-Paar in ein Entwurfsmuster zu verwandeln und es überall dort einzusetzen, wo du Platz dafür siehst.

Es ist zwar gut, Standardlösungen zu identifizieren und sie im Hinterkopf zu behalten, wenn du auf ähnliche Probleme stößt, aber es ist gut möglich, dass das neue Problem, auf das du gestoßen bist, nicht genau auf die gleiche Beschreibung passt wie ein älteres Problem. In diesem Fall könnte es passieren, dass du eine suboptimale Lösung implementierst und Ressourcen verschwendest.

Design Patterns sind heute als führende Beispiele für Problemlösungspaare etabliert, weil sie im Laufe der Zeit von Hunderten und Tausenden von Programmierern getestet und so weit wie möglich verallgemeinert wurden. Wenn du versuchst, diesen Aufwand zu wiederholen, indem du dir einfach eine Reihe von Problemen und Lösungen ansiehst und sie als ähnlich bezeichnest, könntest du deinem Code viel mehr Schaden zufügen, als du jemals erwartet hättest.

Wann solltest du Entwurfsmuster verwenden?

Zusammenfassend gibt es einige Anhaltspunkte, auf die du bei der Verwendung von Design Patterns achten solltest. Nicht alle davon treffen auf die Entwicklung jeder App zu, aber sie sollten dir eine gute Vorstellung davon geben, worauf du achten solltest, wenn du Design Patterns verwenden willst:

  • Du hast ein starkes internes Entwicklerteam, das sich mit Design Patterns gut auskennt.
  • Du hältst dich an ein SDLC-Modell, das Raum für ausführliche Diskussionen über die Architektur deiner App lässt, und bei diesen Diskussionen sind Design Patterns zur Sprache gekommen.
  • Dieselbe Problemstellung ist in deinen Design-Diskussionen mehrfach aufgetaucht und du kennst das passende Design Pattern für diesen Fall.
  • Du hast versucht, eine kleinere Variante deines Problems mit dem Entwurfsmuster unabhängig zu lösen.
  • Mit dem Entwurfsmuster sieht dein Code nicht übermäßig komplex aus.

Wenn ein Entwurfsmuster dein Problem löst und dir dabei hilft, einen einfachen, wiederverwendbaren, modularen, lose gekoppelten Code zu schreiben, der nicht nach Code riecht, ist es vielleicht der richtige Weg.

Ein weiterer guter Tipp ist, nicht alles über Entwurfsmuster zu machen. Entwurfsmuster sollen dir helfen, Probleme zu lösen. Sie sind keine Gesetze, an die du dich halten musst, oder Regeln, die du strikt befolgen musst. Die wichtigsten Regeln und Gesetze sind immer noch dieselben: Halte deinen Code sauber, einfach, lesbar und skalierbar. Wenn ein Entwurfsmuster dir dabei hilft, dein Problem zu lösen, solltest du damit gut zurechtkommen.

Zusammenfassung

JavaScript-Entwurfsmuster sind eine wunderbare Methode, um Probleme anzugehen, mit denen schon viele Programmierer/innen im Laufe der Zeit konfrontiert wurden. Sie stellen bewährte Lösungen dar, die darauf abzielen, deine Codebasis sauber und lose gekoppelt zu halten.

Heutzutage gibt es Hunderte von Entwurfsmustern, die fast jedes Problem lösen, auf das du bei der Entwicklung von Anwendungen stößt. Aber nicht jedes Entwurfsmuster löst dein Problem wirklich jedes Mal.

Wie jede andere Programmierkonvention sind Entwurfsmuster als Vorschläge zur Lösung von Problemen zu verstehen. Sie sind keine Gesetze, die du immer befolgen musst, und wenn du sie wie Gesetze behandelst, könntest du deinen Anwendungen am Ende großen Schaden zufügen.

Sobald deine App fertig ist, brauchst du einen Ort, an dem du sie hosten kannst – und die Anwendungs Hosting-Lösungen von Kinsta gehören zu den schnellsten, zuverlässigsten und sichersten. Du musst dich nur in deinem MyKinsta-Konto (dem benutzerdefinierten Verwaltungs-Dashboard von Kinsta) anmelden, dich mit deinem GitHub-Repository verbinden und loslegen! Außerdem werden dir nur die Ressourcen berechnet, die deine App nutzt.

Welche Entwurfsmuster verwendest du regelmäßig bei deiner Arbeit als Softwareprogrammierer/in? Oder gibt es ein Muster, das wir in dieser Liste übersehen haben? Lass es uns in den Kommentaren unten wissen!

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.