Der umfassende Leitfaden zu JavaScript-Entwurfsmustern
Veröffentlicht: 2022-03-11Als guter JavaScript-Entwickler streben Sie danach, sauberen, gesunden und wartbaren Code zu schreiben. Sie lösen interessante Herausforderungen, die zwar einzigartig sind, aber nicht unbedingt einzigartige Lösungen erfordern. Wahrscheinlich haben Sie schon einmal Code geschrieben, der ähnlich aussieht wie die Lösung eines ganz anderen Problems, das Sie zuvor behandelt haben. Sie wissen es vielleicht nicht, aber Sie haben ein JavaScript -Entwurfsmuster verwendet . Entwurfsmuster sind wiederverwendbare Lösungen für häufig auftretende Probleme im Softwaredesign.
Während der Lebensdauer einer Sprache werden viele solcher wiederverwendbaren Lösungen von einer großen Anzahl von Entwicklern aus der Community dieser Sprache erstellt und getestet. Aufgrund dieser kombinierten Erfahrung vieler Entwickler sind solche Lösungen so nützlich, weil sie uns helfen, Code auf optimierte Weise zu schreiben und gleichzeitig das vorliegende Problem zu lösen.
Die wichtigsten Vorteile, die wir aus Designmustern ziehen, sind die folgenden:
- Sie sind bewährte Lösungen: Da Entwurfsmuster häufig von vielen Entwicklern verwendet werden, können Sie sicher sein, dass sie funktionieren. Und nicht nur das, Sie können sicher sein, dass sie mehrfach überarbeitet und vermutlich Optimierungen vorgenommen wurden.
- Sie sind leicht wiederverwendbar: Entwurfsmuster dokumentieren eine wiederverwendbare Lösung, die modifiziert werden kann, um mehrere bestimmte Probleme zu lösen, da sie nicht an ein bestimmtes Problem gebunden sind.
- Sie sind ausdrucksstark: Design Patterns können eine große Lösung recht elegant erklären.
- Sie erleichtern die Kommunikation: Wenn Entwickler mit Entwurfsmustern vertraut sind, können sie sich leichter über mögliche Lösungen für ein bestimmtes Problem austauschen.
- Sie verhindern, dass Code umgestaltet werden muss: Wenn eine Anwendung unter Berücksichtigung von Entwurfsmustern geschrieben wird, ist es oft so, dass Sie den Code später nicht umgestalten müssen, da die Anwendung des richtigen Entwurfsmusters auf ein bestimmtes Problem bereits optimal ist Lösung.
- Sie verringern die Größe der Codebasis: Da Entwurfsmuster normalerweise elegante und optimale Lösungen sind, benötigen sie normalerweise weniger Code als andere Lösungen.
Ich weiß, dass Sie an dieser Stelle bereit sind, einzusteigen, aber bevor Sie alles über Designmuster lernen, lassen Sie uns einige JavaScript-Grundlagen wiederholen.
Eine kurze Geschichte von JavaScript
JavaScript ist heute eine der beliebtesten Programmiersprachen für die Webentwicklung. Es wurde ursprünglich als eine Art „Klebstoff“ für verschiedene angezeigte HTML-Elemente, bekannt als clientseitige Skriptsprache, für einen der ersten Webbrowser erstellt. Netscape Navigator genannt, konnte es damals nur statisches HTML anzeigen. Wie Sie vielleicht vermuten, führte die Idee einer solchen Skriptsprache damals zu Browserkriegen zwischen den großen Playern der Browserentwicklungsbranche, wie Netscape Communications (heute Mozilla), Microsoft und anderen.
Jeder der großen Player wollte seine eigene Implementierung dieser Skriptsprache durchsetzen, also machte Netscape JavaScript (eigentlich Brendan Eich), Microsoft machte JScript und so weiter. Wie Sie sich vorstellen können, waren die Unterschiede zwischen diesen Implementierungen groß, sodass die Entwicklung für Webbrowser pro Browser durchgeführt wurde, mit den am besten sichtbaren Aufklebern, die mit einer Webseite geliefert wurden. Schnell war klar, dass wir eine standardisierte, browserübergreifende Lösung brauchten, die den Entwicklungsprozess vereinheitlicht und die Erstellung von Webseiten vereinfacht. Was sie sich ausgedacht haben, heißt ECMAScript.
ECMAScript ist eine standardisierte Skriptsprachenspezifikation, die alle modernen Browser zu unterstützen versuchen, und es gibt mehrere Implementierungen (man könnte sagen Dialekte) von ECMAScript. Am beliebtesten ist das Thema dieses Artikels, JavaScript. Seit seiner ersten Veröffentlichung hat ECMAScript viele wichtige Dinge standardisiert, und für diejenigen, die sich mehr für die Einzelheiten interessieren, gibt es eine detaillierte Liste standardisierter Elemente für jede Version des ECMAScript, die auf Wikipedia verfügbar ist. Die Browserunterstützung für ECMAScript-Versionen 6 (ES6) und höher ist noch unvollständig und muss auf ES5 transpiliert werden, um vollständig unterstützt zu werden.
Was ist JavaScript?
Um den Inhalt dieses Artikels vollständig zu verstehen, wollen wir eine Einführung in einige sehr wichtige Sprachmerkmale geben, die wir kennen müssen, bevor wir uns mit JavaScript-Designmustern befassen. Wenn Sie jemand fragen würde: "Was ist JavaScript?" Sie könnten irgendwo in den Zeilen antworten:
JavaScript ist eine leichtgewichtige, interpretierte, objektorientierte Programmiersprache mit erstklassigen Funktionen, die am häufigsten als Skriptsprache für Webseiten bekannt ist.
Die oben genannte Definition bedeutet, dass JavaScript-Code einen geringen Speicherverbrauch hat, einfach zu implementieren und leicht zu erlernen ist, mit einer Syntax, die populären Sprachen wie C++ und Java ähnelt. Es ist eine Skriptsprache, was bedeutet, dass ihr Code interpretiert statt kompiliert wird. Es unterstützt prozedurale, objektorientierte und funktionale Programmierstile, was es für Entwickler sehr flexibel macht.
Bisher haben wir uns alle Eigenschaften angesehen, die wie viele andere Sprachen da draußen klingen, also werfen wir einen Blick auf die Besonderheiten von JavaScript in Bezug auf andere Sprachen. Ich werde einige Merkmale auflisten und mein Bestes geben, um zu erklären, warum sie besondere Aufmerksamkeit verdienen.
JavaScript unterstützt erstklassige Funktionen
Diese Eigenschaft war für mich früher schwer zu verstehen, als ich gerade mit JavaScript anfing, da ich aus einem C/C++-Hintergrund kam. JavaScript behandelt Funktionen als erstklassige Bürger, was bedeutet, dass Sie Funktionen wie jede andere Variable als Parameter an andere Funktionen übergeben können.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
JavaScript ist prototypbasiert
Wie viele andere objektorientierte Sprachen unterstützt JavaScript Objekte, und einer der ersten Begriffe, die einem in den Sinn kommen, wenn man an Objekte denkt, ist Klassen und Vererbung. Hier wird es ein wenig knifflig, da die Sprache keine Klassen in ihrer reinen Sprachform unterstützt, sondern etwas verwendet, das als prototypbasierte oder instanzbasierte Vererbung bezeichnet wird.
Gerade jetzt, in ES6, wird der formale Begriff Klasse eingeführt, was bedeutet, dass die Browser dies immer noch nicht unterstützen (wenn Sie sich erinnern, dass die letzte vollständig unterstützte ECMAScript-Version zum Zeitpunkt des Schreibens 5.1 ist). Es ist jedoch wichtig zu beachten, dass, obwohl der Begriff „Klasse“ in JavaScript eingeführt wird, es immer noch prototypbasierte Vererbung unter der Haube verwendet.
Die auf Prototypen basierende Programmierung ist ein Stil der objektorientierten Programmierung, bei der die Wiederverwendung von Verhalten (bekannt als Vererbung) über einen Prozess der Wiederverwendung vorhandener Objekte über Delegationen erfolgt, die als Prototypen dienen. Wir werden darauf näher eingehen, sobald wir zum Abschnitt „Entwurfsmuster“ des Artikels kommen, da diese Eigenschaft in vielen JavaScript-Entwurfsmustern verwendet wird.
JavaScript-Ereignisschleifen
Wenn Sie Erfahrung im Umgang mit JavaScript haben, ist Ihnen sicherlich der Begriff Callback-Funktion geläufig. Für diejenigen, die mit dem Begriff nicht vertraut sind: Eine Callback-Funktion ist eine Funktion, die als Parameter (denken Sie daran, dass JavaScript Funktionen als erstklassige Bürger behandelt) an eine andere Funktion gesendet und nach dem Auslösen eines Ereignisses ausgeführt wird. Dies wird normalerweise verwendet, um Ereignisse wie einen Mausklick oder das Drücken einer Tastaturtaste zu abonnieren.
Jedes Mal, wenn ein Ereignis, an das ein Listener angehängt ist, ausgelöst wird (ansonsten geht das Ereignis verloren), wird eine Nachricht an eine Warteschlange von Nachrichten gesendet, die synchron verarbeitet werden, in einer FIFO-Manier (first-in-first-out ). Dies wird als Ereignisschleife bezeichnet .
Jeder der Nachrichten in der Warteschlange ist eine Funktion zugeordnet. Sobald eine Nachricht aus der Warteschlange entfernt wurde, führt die Laufzeit die Funktion vollständig aus, bevor eine andere Nachricht verarbeitet wird. Das heißt, wenn eine Funktion andere Funktionsaufrufe enthält, werden diese alle ausgeführt, bevor eine neue Nachricht aus der Warteschlange verarbeitet wird. Dies wird Run-to-Completion genannt.
while (queue.waitForMessage()) { queue.processNextMessage(); }
Die queue.waitForMessage()
wartet synchron auf neue Nachrichten. Jede der verarbeiteten Nachrichten hat ihren eigenen Stapel und wird verarbeitet, bis der Stapel leer ist. Nach Abschluss wird eine neue Nachricht aus der Warteschlange verarbeitet, sofern vorhanden.
Sie haben vielleicht auch gehört, dass JavaScript nicht blockiert, was bedeutet, dass das Programm bei der Ausführung einer asynchronen Operation andere Dinge verarbeiten kann, z. B. das Empfangen von Benutzereingaben, während es auf den Abschluss der asynchronen Operation wartet, ohne die Hauptoperation zu blockieren Ausführungsthread. Dies ist eine sehr nützliche Eigenschaft von JavaScript und es könnte ein ganzer Artikel nur zu diesem Thema geschrieben werden; Dies liegt jedoch außerhalb des Geltungsbereichs dieses Artikels.
Was sind Designmuster?
Wie ich bereits sagte, sind Entwurfsmuster wiederverwendbare Lösungen für häufig auftretende Probleme im Softwaredesign. Werfen wir einen Blick auf einige der Kategorien von Entwurfsmustern.
Proto-Muster
Wie erstellt man ein Muster? Nehmen wir an, Sie haben ein häufig auftretendes Problem erkannt und haben Ihre eigene einzigartige Lösung für dieses Problem, die nicht weltweit anerkannt und dokumentiert ist. Sie verwenden diese Lösung jedes Mal, wenn Sie auf dieses Problem stoßen, und Sie denken, dass sie wiederverwendbar ist und dass die Entwicklergemeinschaft davon profitieren könnte.
Wird daraus sofort ein Muster? Zum Glück nein. Oft hat man gute Praktiken beim Schreiben von Code und verwechselt einfach etwas, das wie ein Muster aussieht, mit einem solchen, obwohl es in Wirklichkeit kein Muster ist.
Wie können Sie wissen, ob das, was Sie zu erkennen glauben, tatsächlich ein Designmuster ist?
Indem Sie die Meinung anderer Entwickler dazu einholen, indem Sie den Prozess der Erstellung eines Musters selbst kennen und sich mit bestehenden Mustern vertraut machen. Es gibt eine Phase, die ein Muster durchlaufen muss, bevor es zu einem vollwertigen Muster wird, und dies wird als Proto-Muster bezeichnet.
Ein Proto-Pattern ist ein Pattern-to-be, wenn es eine bestimmte Testphase durch verschiedene Entwickler und Szenarien besteht, in denen sich das Pattern als nützlich erweist und korrekte Ergebnisse liefert. Es gibt eine ziemlich große Menge an Arbeit und Dokumentation – von denen die meisten den Rahmen dieses Artikels sprengen würden – um ein vollwertiges Muster zu schaffen, das von der Community anerkannt wird.
Anti-Muster
Da ein Entwurfsmuster eine gute Praxis darstellt, repräsentiert ein Anti-Pattern eine schlechte Praxis.
Ein Beispiel für ein Anti-Pattern wäre das Modifizieren des Prototyps der Object
-Klasse. Fast alle Objekte in JavaScript erben von Object
(denken Sie daran, dass JavaScript prototypbasierte Vererbung verwendet), stellen Sie sich also ein Szenario vor, in dem Sie diesen Prototyp geändert haben. Änderungen am Object
Prototyp würden in allen Objekten sichtbar, die von diesem Prototyp erben – das wären die meisten JavaScript-Objekte . Dies ist eine Katastrophe, die darauf wartet, passiert zu werden.
Ein weiteres Beispiel, ähnlich dem oben erwähnten, ist das Modifizieren von Objekten, die Sie nicht besitzen. Ein Beispiel hierfür wäre das Überschreiben einer Funktion aus einem Objekt, das in vielen Szenarien in der gesamten Anwendung verwendet wird. Wenn Sie mit einem großen Team arbeiten, stellen Sie sich die Verwirrung vor, die dies verursachen würde; Sie würden schnell auf Namenskollisionen, inkompatible Implementierungen und Wartungsalpträume stoßen.
So wie es nützlich ist, alle guten Praktiken und Lösungen zu kennen, ist es auch sehr wichtig, auch die schlechten zu kennen. Auf diese Weise können Sie sie erkennen und Fehler im Voraus vermeiden.
Kategorisierung von Entwurfsmustern
Designmuster können auf verschiedene Arten kategorisiert werden, aber die beliebteste ist die folgende:
- Kreative Designmuster
- Strukturelle Entwurfsmuster
- Verhaltensdesignmuster
- Designmuster für Parallelität
- Architektonische Gestaltungsmuster
Kreative Designmuster
Diese Muster befassen sich mit Objekterzeugungsmechanismen, die die Objekterzeugung im Vergleich zu einem grundlegenden Ansatz optimieren. Die Grundform der Objekterzeugung könnte zu Entwurfsproblemen oder zu einer zusätzlichen Komplexität des Entwurfs führen. Gestaltungsmuster lösen dieses Problem, indem sie die Objekterstellung irgendwie steuern. Einige der beliebtesten Designmuster in dieser Kategorie sind:
- Fabrikmethode
- Abstrakte Fabrik
- Baumeister
- Prototyp
- Einzelling
Strukturelle Entwurfsmuster
Diese Muster befassen sich mit Objektbeziehungen. Sie stellen sicher, dass sich nicht das gesamte System mit ändern muss, wenn sich ein Teil eines Systems ändert. Die beliebtesten Muster in dieser Kategorie sind:
- Adapter
- Brücke
- Zusammengesetzt
- Dekorateur
- Fassade
- Fliegengewicht
- Proxy
Verhaltensdesignmuster
Diese Arten von Mustern erkennen, implementieren und verbessern die Kommunikation zwischen unterschiedlichen Objekten in einem System. Sie tragen dazu bei sicherzustellen, dass unterschiedliche Teile eines Systems über synchronisierte Informationen verfügen. Beliebte Beispiele für diese Muster sind:
- Verantwortungskette
- Befehl
- Iterator
- Vermittler
- Erinnerung
- Beobachter
- Bundesland
- Strategie
- Besucher
Designmuster für Parallelität
Diese Arten von Entwurfsmustern befassen sich mit Multithread-Programmierparadigmen. Einige der beliebtesten sind:
- Aktives Objekt
- Kernreaktion
- Planer
Architektonische Gestaltungsmuster
Entwurfsmuster, die für architektonische Zwecke verwendet werden. Einige der bekanntesten sind:
- MVC (Model-View-Controller)
- MVP (Model-View-Presenter)
- MVVM (Model-View-ViewModel)
Im folgenden Abschnitt werden wir einige der oben genannten Entwurfsmuster genauer betrachten und Beispiele zum besseren Verständnis bereitstellen.
Beispiele für Designmuster
Jedes der Entwurfsmuster repräsentiert eine bestimmte Art von Lösung für eine bestimmte Art von Problem. Es gibt keinen universellen Satz von Mustern, der immer am besten passt. Wir müssen lernen, wann sich ein bestimmtes Muster als nützlich erweist und ob es einen tatsächlichen Wert bietet. Sobald wir mit den Mustern und Szenarien vertraut sind, für die sie am besten geeignet sind, können wir leicht feststellen, ob ein bestimmtes Muster für ein bestimmtes Problem gut geeignet ist oder nicht.
Denken Sie daran, dass die Anwendung des falschen Musters auf ein bestimmtes Problem zu unerwünschten Effekten wie unnötiger Codekomplexität, unnötigem Mehraufwand für die Leistung oder sogar der Entstehung eines neuen Antimusters führen kann.
Dies sind alles wichtige Dinge, die Sie berücksichtigen sollten, wenn Sie darüber nachdenken, ein Entwurfsmuster auf unseren Code anzuwenden. Wir werden uns einige der Designmuster ansehen, die ich persönlich nützlich fand und mit denen jeder erfahrene JavaScript-Entwickler vertraut sein sollte.
Konstruktormuster
Wenn man an klassische objektorientierte Sprachen denkt, ist ein Konstruktor eine spezielle Funktion in einer Klasse, die ein Objekt mit einer Reihe von Standard- und/oder gesendeten Werten initialisiert.
Übliche Methoden zum Erstellen von Objekten in JavaScript sind die drei folgenden Methoden:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
Nach dem Erstellen eines Objekts gibt es (seit ES3) vier Möglichkeiten, diesen Objekten Eigenschaften hinzuzufügen. Sie sind die folgenden:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
Die beliebteste Methode zum Erstellen von Objekten sind die geschweiften Klammern und zum Hinzufügen von Eigenschaften die Punktnotation oder eckige Klammern. Jeder, der Erfahrung mit JavaScript hat, hat sie verwendet.
Wir haben bereits erwähnt, dass JavaScript keine nativen Klassen unterstützt, aber es unterstützt Konstruktoren durch die Verwendung eines „new“-Schlüsselworts, das einem Funktionsaufruf vorangestellt wird. Auf diese Weise können wir die Funktion als Konstruktor verwenden und ihre Eigenschaften genauso initialisieren wie mit einem klassischen Sprachkonstruktor.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Allerdings gibt es hier noch Raum für Verbesserungen. Wenn Sie sich erinnern, habe ich bereits erwähnt, dass JavaScript prototypbasierte Vererbung verwendet. Das Problem beim vorherigen Ansatz besteht darin, dass die Methode writesCode
für jede der Instanzen des Person
-Konstruktors neu definiert wird. Wir können dies vermeiden, indem wir die Methode in den Funktionsprototyp setzen:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Jetzt können beide Instanzen des Person
-Konstruktors auf eine gemeinsame Instanz der Methode writesCode()
.

Modulmuster
Was Besonderheiten angeht, überrascht JavaScript immer wieder. Eine weitere Besonderheit von JavaScript (zumindest was objektorientierte Sprachen betrifft) ist, dass JavaScript keine Zugriffsmodifikatoren unterstützt. In einer klassischen OOP-Sprache definiert ein Benutzer eine Klasse und legt Zugriffsrechte für ihre Mitglieder fest. Da JavaScript in seiner einfachen Form weder Klassen noch Zugriffsmodifikatoren unterstützt, haben JavaScript-Entwickler einen Weg gefunden, dieses Verhalten bei Bedarf nachzuahmen.
Bevor wir auf die Besonderheiten des Modulmusters eingehen, lassen Sie uns über das Konzept der Schließung sprechen. Eine Schließung ist eine Funktion mit Zugriff auf den übergeordneten Gültigkeitsbereich, auch nachdem die übergeordnete Funktion geschlossen wurde. Sie helfen uns, das Verhalten von Zugriffsmodifikatoren durch Scoping nachzuahmen. Lassen Sie uns dies anhand eines Beispiels zeigen:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
Wie Sie sehen können, haben wir durch die Verwendung von IIFE die Zählervariable an eine Funktion gebunden, die aufgerufen und geschlossen wurde, auf die jedoch immer noch von der untergeordneten Funktion zugegriffen werden kann, die sie erhöht. Da wir von außerhalb des Funktionsausdrucks nicht auf die Zählervariable zugreifen können, haben wir sie durch Scoping-Manipulation privat gemacht.
Mit den Closures können wir Objekte mit privaten und öffentlichen Teilen erstellen. Diese werden Module genannt und sind sehr nützlich, wenn wir bestimmte Teile eines Objekts ausblenden und nur eine Schnittstelle für den Benutzer des Moduls freigeben möchten. Lassen Sie uns dies an einem Beispiel zeigen:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
Das Nützlichste, was dieses Muster einführt, ist die klare Trennung von privaten und öffentlichen Teilen eines Objekts, ein Konzept, das Entwicklern mit einem klassischen objektorientierten Hintergrund sehr ähnlich ist.
Allerdings ist nicht alles so perfekt. Wenn Sie die Sichtbarkeit eines Mitglieds ändern möchten, müssen Sie den Code überall dort ändern, wo Sie dieses Mitglied verwendet haben, da der Zugriff auf öffentliche und private Teile unterschiedlich ist. Außerdem können Methoden, die dem Objekt nach ihrer Erstellung hinzugefügt wurden, nicht auf die privaten Mitglieder des Objekts zugreifen.
Aufschlussreiches Modulmuster
Dieses Muster ist eine Verbesserung des oben dargestellten Modulmusters. Der Hauptunterschied besteht darin, dass wir die gesamte Objektlogik im privaten Bereich des Moduls schreiben und dann einfach die Teile, die öffentlich sein sollen, verfügbar machen, indem wir ein anonymes Objekt zurückgeben. Wir können auch die Benennung privater Mitglieder ändern, wenn wir private Mitglieder ihren entsprechenden öffentlichen Mitgliedern zuordnen.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
Das aufschlussreiche Modulmuster ist eine von mindestens drei Möglichkeiten, wie wir ein Modulmuster implementieren können. Die Unterschiede zwischen dem offenbarenden Modulmuster und den anderen Varianten des Modulmusters liegen hauptsächlich darin, wie auf öffentliche Elemente verwiesen wird. Infolgedessen ist das aufschlussreiche Modulmuster viel einfacher zu verwenden und zu modifizieren; Es kann sich jedoch in bestimmten Szenarios als anfällig erweisen, z. B. bei der Verwendung von RMP-Objekten als Prototypen in einer Vererbungskette. Die problematischen Situationen sind die folgenden:
- Wenn wir eine private Funktion haben, die sich auf eine öffentliche Funktion bezieht, können wir die öffentliche Funktion nicht überschreiben, da die private Funktion weiterhin auf die private Implementierung der Funktion verweist und somit einen Fehler in unser System einführt.
- Wenn wir ein öffentliches Mitglied haben, das auf eine private Variable zeigt, und versuchen, das öffentliche Mitglied von außerhalb des Moduls zu überschreiben, würden die anderen Funktionen immer noch auf den privaten Wert der Variablen verweisen und einen Fehler in unser System einführen.
Singleton-Muster
Das Singleton-Muster wird in Szenarien verwendet, in denen wir genau eine Instanz einer Klasse benötigen. Zum Beispiel brauchen wir ein Objekt, das eine Konfiguration für etwas enthält. In diesen Fällen ist es nicht erforderlich, jedes Mal, wenn das Konfigurationsobjekt irgendwo im System benötigt wird, ein neues Objekt zu erstellen.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
Wie Sie im Beispiel sehen können, ist die generierte Zufallszahl immer dieselbe, ebenso wie die gesendeten Konfigurationswerte.
Es ist wichtig zu beachten, dass der Zugriffspunkt zum Abrufen des Singleton-Werts nur einer und sehr bekannt sein muss. Ein Nachteil bei der Verwendung dieses Musters ist, dass es ziemlich schwierig zu testen ist.
Beobachtermuster
Das Beobachtermuster ist ein sehr nützliches Werkzeug, wenn wir ein Szenario haben, in dem wir die Kommunikation zwischen unterschiedlichen Teilen unseres Systems auf optimierte Weise verbessern müssen. Es fördert eine lose Kopplung zwischen Objekten.
Es gibt verschiedene Versionen dieses Musters, aber in seiner grundlegendsten Form haben wir zwei Hauptteile des Musters. Das erste ist ein Subjekt und das zweite sind Beobachter.
Ein Subjekt behandelt alle Operationen bezüglich eines bestimmten Themas, das die Beobachter abonnieren. Diese Operationen abonnieren einen Beobachter für ein bestimmtes Thema, kündigen einen Beobachter von einem bestimmten Thema und benachrichtigen Beobachter über ein bestimmtes Thema, wenn ein Ereignis veröffentlicht wird.
Es gibt jedoch eine Variation dieses Musters, das sogenannte Publisher/Subscriber-Muster, das ich in diesem Abschnitt als Beispiel verwenden werde. Der Hauptunterschied zwischen einem klassischen Beobachtermuster und dem Publisher/Subscriber-Muster besteht darin, dass Publisher/Subscriber eine noch lockerere Kopplung fördert als das Observer-Muster.
Im Observer-Muster enthält das Subjekt die Referenzen zu den abonnierten Observern und ruft Methoden direkt von den Objekten selbst auf, während wir im Publisher/Subscriber-Muster Kanäle haben, die als Kommunikationsbrücke zwischen einem Subscriber und einem Publisher dienen. Der Herausgeber löst ein Ereignis aus und führt einfach die für dieses Ereignis gesendete Callback-Funktion aus.
Ich werde ein kurzes Beispiel des Publisher/Subscriber-Musters zeigen, aber für Interessierte kann ein klassisches Beobachtermuster-Beispiel leicht online gefunden werden.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
Dieses Entwurfsmuster ist in Situationen nützlich, in denen wir mehrere Operationen für ein einzelnes ausgelöstes Ereignis ausführen müssen. Stellen Sie sich vor, Sie haben ein Szenario, in dem wir mehrere AJAX-Aufrufe an einen Back-End-Dienst senden und dann je nach Ergebnis weitere AJAX-Aufrufe ausführen müssen. Sie müssten die AJAX-Aufrufe ineinander verschachteln und möglicherweise in eine Situation eintreten, die als Callback-Hölle bekannt ist. Die Verwendung des Publisher/Subscriber-Musters ist eine viel elegantere Lösung.
Ein Nachteil bei der Verwendung dieses Musters ist das schwierige Testen verschiedener Teile unseres Systems. Es gibt keinen eleganten Weg für uns festzustellen, ob sich die abonnierenden Teile des Systems wie erwartet verhalten oder nicht.
Mediator-Muster
Wir werden kurz auf ein Muster eingehen, das auch sehr nützlich ist, wenn es um entkoppelte Systeme geht. Wenn wir ein Szenario haben, in dem mehrere Teile eines Systems kommunizieren und koordiniert werden müssen, wäre es vielleicht eine gute Lösung, einen Mediator einzuführen.
Ein Mediator ist ein Objekt, das als zentraler Punkt für die Kommunikation zwischen unterschiedlichen Teilen eines Systems verwendet wird und den Arbeitsablauf zwischen ihnen abwickelt. Nun ist es wichtig zu betonen, dass es den Workflow handhabt. Warum ist das wichtig?
Denn es besteht eine große Ähnlichkeit mit dem Publisher/Subscriber-Muster. Sie könnten sich fragen, OK, also helfen diese beiden Muster, eine bessere Kommunikation zwischen Objekten zu implementieren … Was ist der Unterschied?
Der Unterschied besteht darin, dass ein Mediator den Workflow übernimmt, während der Herausgeber/Abonnent eine sogenannte „Fire and Forget“-Kommunikation verwendet. Der Herausgeber/Abonnent ist einfach ein Ereignisaggregator, d. h. er kümmert sich lediglich darum, die Ereignisse auszulösen und die richtigen Abonnenten darüber zu informieren, welche Ereignisse ausgelöst wurden. Dem Ereignisaggregator ist es egal, was passiert, nachdem ein Ereignis ausgelöst wurde, was bei einem Vermittler nicht der Fall ist.
Ein nettes Beispiel für einen Vermittler ist eine assistentenartige Schnittstelle. Angenommen, Sie haben einen umfangreichen Registrierungsprozess für ein System, an dem Sie gearbeitet haben. Wenn viele Informationen von einem Benutzer benötigt werden, empfiehlt es sich oft, diese in mehrere Schritte aufzuteilen.
Auf diese Weise wird der Code viel sauberer (einfacher zu pflegen) und der Benutzer wird nicht von der Menge an Informationen überwältigt, die angefordert werden, nur um die Registrierung abzuschließen. Ein Vermittler ist ein Objekt, das die Registrierungsschritte unter Berücksichtigung verschiedener möglicher Arbeitsabläufe abwickeln würde, die aufgrund der Tatsache auftreten könnten, dass jeder Benutzer potenziell einen einzigartigen Registrierungsprozess haben könnte.
Der offensichtliche Vorteil dieses Entwurfsmusters ist eine verbesserte Kommunikation zwischen verschiedenen Teilen eines Systems, die jetzt alle über den Mediator und eine sauberere Codebasis kommunizieren.
Ein Nachteil wäre, dass wir jetzt einen Single Point of Failure in unser System eingeführt haben, was bedeutet, dass wenn unser Mediator ausfällt, das gesamte System nicht mehr funktionieren könnte.
Prototyp-Muster
Wie wir bereits im gesamten Artikel erwähnt haben, unterstützt JavaScript keine Klassen in seiner nativen Form. Die Vererbung zwischen Objekten wird unter Verwendung von prototypbasierter Programmierung implementiert.
Es ermöglicht uns, Objekte zu erstellen, die als Prototyp für andere erstellte Objekte dienen können. Das Prototypobjekt wird als Blaupause für jedes Objekt verwendet, das der Konstruktor erstellt.
Da wir bereits in den vorherigen Abschnitten darüber gesprochen haben, wollen wir ein einfaches Beispiel zeigen, wie dieses Muster verwendet werden könnte.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
Nächste Schritte
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.