1. Code
  2. JavaScript

Design Patterns in JavaScript verstehen

Scroll to top
13 min read

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Heute werden wir unsere Computer-Hüte anziehen, wenn wir etwas über gängige Designmuster lernen. Entwurfsmuster bieten Entwicklern Möglichkeiten, technische Probleme auf wiederverwendbare und elegante Weise zu lösen. Möchten Sie ein besserer JavaScript-Entwickler werden? Dann lesen Sie weiter.

Neu veröffentlichtes Tutorial

Alle paar Wochen besuchen wir einige der Lieblingsbeiträge unseres Lesers aus der gesamten Geschichte der Website. Dieses Tutorial wurde erstmals im Juli 2012 veröffentlicht.


Einführung

Solide Entwurfsmuster sind der Grundbaustein für wartbare Softwareanwendungen. Wenn Sie jemals an einem technischen Interview teilgenommen haben, wurden Sie gerne über sie gefragt. In diesem Tutorial sehen wir uns einige Muster an, die Sie heute verwenden können.


Was ist ein Designmuster?

Ein Entwurfsmuster ist eine wiederverwendbare Softwarelösung

Ein Entwurfsmuster ist einfach eine wiederverwendbare Softwarelösung für eine bestimmte Art von Problem, die häufig bei der Entwicklung von Software auftritt. In den vielen Jahren der Softwareentwicklung haben Experten Wege gefunden, ähnliche Probleme zu lösen. Diese Lösungen wurden in Entwurfsmuster eingekapselt. Damit:

  • Muster sind bewährte Lösungen für Softwareentwicklungsprobleme
  • Muster sind skalierbar, da sie normalerweise strukturiert sind und Regeln enthalten, denen Sie folgen sollten
  • Muster sind für ähnliche Probleme wiederverwendbar

Wir werden einige Beispiele für Designmuster im Tutorial kennenlernen.


Arten von Entwurfsmustern

In der Softwareentwicklung werden Entwurfsmuster im Allgemeinen in einige Kategorien gruppiert. Wir werden die drei wichtigsten in diesem Tutorial behandeln. Sie werden im Folgenden kurz erläutert:

  1. Entwurfsmuster konzentrieren sich auf Möglichkeiten zum Erstellen von Objekten oder Klassen. Dies klingt vielleicht einfach (und in einigen Fällen auch), aber große Anwendungen müssen den Objekt-Erstellungsprozess steuern.

  2. Strukturelle Entwurfsmuster konzentrieren sich auf Möglichkeiten zum Verwalten von Beziehungen zwischen Objekten, sodass Ihre Anwendung skalierbar strukturiert wird. Ein wichtiger Aspekt von Strukturmustern besteht darin, sicherzustellen, dass eine Änderung in einem Teil Ihrer Anwendung nicht alle anderen Teile beeinflusst.

  3. Verhaltensmuster konzentrieren sich auf die Kommunikation zwischen Objekten.

Möglicherweise haben Sie nach dem Lesen dieser kurzen Beschreibungen noch Fragen. Das ist natürlich, und die Dinge werden sich klären, wenn wir unten einige Entwurfsmuster in der Tiefe betrachten. Also lies weiter!


Ein Hinweis zu Klassen in JavaScript

Wenn Sie über Entwurfsmuster lesen, werden häufig Verweise auf Klassen und Objekte angezeigt. Dies kann verwirrend sein, da JavaScript nicht wirklich das Konstrukt "Klasse" hat; Ein korrekterer Begriff ist "Datentyp".

Datentypen in JavaScript

JavaScript ist eine objektorientierte Sprache, in der Objekte von anderen Objekten in einem Konzept namens prototypische Vererbung erben. Ein Datentyp kann erstellt werden, indem eine so genannte Konstruktorfunktion wie folgt definiert wird:

1
function Person(config) {
2
    this.name = config.name;
3
    this.age = config.age;
4
}
5
6
Person.prototype.getAge = function() {
7
    return this.age;
8
};
9
10
var tilo = new Person({name:"Tilo", age:23 });
11
console.log(tilo.getAge());

Beachten Sie die Verwendung des prototype beim Definieren von Methoden für den Datentyp Person. Da mehrere Person-Objekte auf denselben Prototyp verweisen, kann die getAge() -Methode von allen Instanzen des Datentyps Person gemeinsam verwendet werden, anstatt sie für jede Instanz neu zu definieren. Darüber hinaus hat jeder Datentyp, der von Person erbt, Zugriff auf die Methode getAge().

Umgang mit Privatsphäre

Ein anderes häufiges Problem in JavaScript ist, dass es keine echte Bedeutung von privaten Variablen gibt. Wir können jedoch Verschlüsse verwenden, um die Privatsphäre etwas zu simulieren. Betrachten Sie das folgende Snippet:

1
var retinaMacbook = (function() {
2
3
    //Private variables

4
    var RAM, addRAM;
5
6
    RAM = 4;
7
8
    //Private method

9
    addRAM = function (additionalRAM) {
10
        RAM += additionalRAM;
11
    };
12
13
    return {
14
15
        //Public variables and methods

16
        USB: undefined,
17
        insertUSB: function (device) {
18
            this.USB = device;
19
        },
20
21
        removeUSB: function () {
22
            var device = this.USB;
23
            this.USB = undefined;
24
            return device;
25
        }
26
    };
27
})();

Im obigen Beispiel haben wir ein RetinaMacbook-Objekt mit öffentlichen und privaten Variablen und Methoden erstellt. So würden wir es benutzen:

1
retinaMacbook.insertUSB("myUSB");
2
console.log(retinaMacbook.USB); //logs out "myUSB"

3
console.log(retinaMacbook.RAM) //logs out undefined

Es gibt viel mehr, was wir mit Funktionen und Closures in JavaScript tun können, aber wir werden in diesem Tutorial nicht auf alles eingehen. Mit dieser kleinen Lektion über JavaScript-Datentypen und Privatsphäre hinter uns können wir uns mit Designmustern vertraut machen.


Gestaltungsmuster

Es gibt viele verschiedene Arten von kreativen Designmustern, aber wir werden zwei von ihnen in diesem Tutorial behandeln: Builder und Prototype. Ich finde, dass diese oft genug verwendet werden, um die Aufmerksamkeit zu rechtfertigen.

Erbauer-Muster

Das Builder-Muster wird häufig in der Webentwicklung verwendet und Sie haben es wahrscheinlich schon vorher verwendet, ohne es zu merken. Einfach gesagt, kann dieses Muster wie folgt definiert werden:

Durch die Anwendung des Builder-Musters können wir Objekte erstellen, indem wir nur den Typ und den Inhalt des Objekts angeben. Wir müssen das Objekt nicht explizit erstellen.

Zum Beispiel haben Sie das wahrscheinlich unzählige Male in jQuery gemacht:

1
var myDiv = $('<div id="myDiv">This is a div.</div>');
2
3
//myDiv now represents a jQuery object referencing a DOM node.

4
5
var someText = $('<p/>');
6
//someText is a jQuery object referencing an HTMLParagraphElement

7
8
var input = $('<input />');

Sehen Sie sich die drei obigen Beispiele an. In der ersten haben wir ein <div/>-Element mit etwas Inhalt übergeben. In der Sekunde haben wir ein leeres <p>-Tag übergeben. In der letzten haben wir ein <input />-Element übergeben. Das Ergebnis aller drei war das gleiche: Es wurde ein jQuery-Objekt zurückgegeben, das auf einen DOM-Knoten verweist.

Die $ Variable übernimmt das Builder-Muster in jQuery. In jedem Beispiel haben wir ein jQuery-DOM-Objekt zurückgegeben und hatten Zugriff auf alle Methoden, die von der jQuery-Bibliothek bereitgestellt werden, aber zu keinem Zeitpunkt haben wir explizit document.createElement aufgerufen. Die JS-Bibliothek bewältigte all das unter der Haube.

Stellen Sie sich vor, wie viel Arbeit es wäre, wenn wir das DOM-Element explizit erstellen und darin Inhalte einfügen müssten! Indem wir das Builder-Muster nutzen, können wir uns auf den Typ und den Inhalt des Objekts konzentrieren, anstatt es explizit zu erstellen.

Prototyp Muster

Zuvor haben wir erklärt, wie man Datentypen in JavaScript über Funktionen definiert und dem prototype des Objekts Methoden hinzufügt. Das Prototyp-Muster erlaubt es Objekten, über ihre Prototypen von anderen Objekten zu erben.

Das Prototypmuster ist ein Muster, bei dem Objekte auf der Grundlage einer Vorlage eines vorhandenen Objekts durch Klonen erzeugt werden.

Dies ist eine einfache und natürliche Möglichkeit, die Vererbung in JavaScript zu implementieren. Beispielsweise:

1
var Person = {
2
    numFeet: 2,
3
    numHeads: 1,
4
    numHands:2
5
};
6
7
//Object.create takes its first argument and applies it to the prototype of your new object.

8
var tilo = Object.create(Person);
9
10
console.log(tilo.numHeads); //outputs 1

11
tilo.numHeads = 2;
12
console.log(tilo.numHeads) //outputs 2

Die Eigenschaften (und Methoden) im Person-Objekt werden auf den Prototyp des tilo-Objekts angewendet. Wir können die Eigenschaften des tilo-Objekts neu definieren, wenn wir möchten, dass sie anders sind.

In the example above, we used Object.create(). Internet Explorer 8 unterstützt jedoch die neuere Methode nicht. In diesen Fällen können wir sein Verhalten simulieren:

1
var vehiclePrototype = {
2
3
  init: function (carModel) {
4
    this.model = carModel;
5
  },
6
7
  getModel: function () {
8
    console.log( "The model of this vehicle is " + this.model);
9
  }
10
};
11
12
13
function vehicle (model) {
14
15
  function F() {};
16
  F.prototype = vehiclePrototype;
17
18
  var f = new F();
19
20
  f.init(model);
21
  return f;
22
23
}
24
25
var car = vehicle("Ford Escort");
26
car.getModel();

Der einzige Nachteil dieser Methode besteht darin, dass Sie keine schreibgeschützten Eigenschaften angeben können, die bei Verwendung von Object.create() angegeben werden können. Das Prototypmuster zeigt jedoch, wie Objekte von anderen Objekten erben können.


Strukturelle Entwurfsmuster

Strukturelle Entwurfsmuster sind sehr hilfreich, um herauszufinden, wie ein System funktionieren sollte. Sie ermöglichen unseren Anwendungen eine einfache Skalierbarkeit und Wartung. Wir werden uns die folgenden Muster in dieser Gruppe ansehen: Composite und Facade.

Zusammengesetztes Muster

Das zusammengesetzte Muster ist ein anderes Muster, das Sie wahrscheinlich vorher ohne irgendeine Realisierung verwendet haben.

Das zusammengesetzte Muster besagt, dass eine Gruppe von Objekten auf dieselbe Weise behandelt werden kann wie ein einzelnes Objekt der Gruppe.

Was bedeutet das? Nun, betrachten Sie dieses Beispiel in jQuery (die meisten JS-Bibliotheken haben eine Entsprechung dazu):

1
$('.myList').addClass('selected');
2
$('#myItem').addClass('selected');
3
4
//dont do this on large tables, it's just an example.

5
$("#dataTable tbody tr").on("click", function(event){
6
    alert($(this).text());
7
});
8
9
$('#myButton').on("click", function(event) {
10
    alert("Clicked.");
11
});

Die meisten JavaScript-Bibliotheken bieten eine konsistente API, unabhängig davon, ob es sich um ein einzelnes DOM-Element oder ein Array von DOM-Elementen handelt. Im ersten Beispiel können wir die selected Klasse zu allen Elementen hinzufügen, die vom Selektor .myList abgerufen werden, aber wir können die gleiche Methode verwenden, wenn wir mit einem einzelnen DOM-Element, #myItem, zu tun haben. Ähnlich können wir Ereignishandler mit der Methode on() an mehreren Knoten oder an einem einzelnen Knoten über die gleiche API anhängen.

Indem wir das Composite-Muster nutzen, stellen wir mit jQuery (und vielen anderen Bibliotheken) eine vereinfachte API bereit.

Das zusammengesetzte Muster kann manchmal auch Probleme verursachen. In einer losen Sprache wie JavaScript kann es oft hilfreich sein zu wissen, ob es sich um ein einzelnes Element oder mehrere Elemente handelt. Da das zusammengesetzte Muster die gleiche API für beide verwendet, können wir oft eine für die andere verwechseln und mit unerwarteten Fehlern enden. Einige Bibliotheken, z. B. YUI3, bieten zwei separate Methoden zum Abrufen von Elementen (Y.one() vs. Y.all()).

Fassadenmuster

Hier ist ein weiteres gemeinsames Muster, das wir für selbstverständlich halten. In der Tat ist dies eine meiner Favoriten, weil es einfach ist, und ich habe gesehen, dass es überall verwendet wird, um mit Browser-Inkonsistenzen zu helfen. Hier ist, was das Fassadenmuster ist:

Das Fassadenmuster bietet dem Benutzer eine einfache Oberfläche, während es die zugrunde liegende Komplexität versteckt.

Das Fassadenmuster verbessert fast immer die Nutzbarkeit einer Software. Wenn Sie jQuery erneut als Beispiel verwenden, ist eine der beliebtesten Methoden der Bibliothek die Methode ready():

1
$(document).ready(function() {
2
3
    //all your code goes here...

4
5
});

Die Methode ready() implementiert tatsächlich eine Fassade. Wenn Sie sich die Quelle ansehen, finden Sie hier Folgendes:

1
ready: (function() {
2
3
    ...
4
5
    //Mozilla, Opera, and Webkit

6
    if (document.addEventListener) {
7
        document.addEventListener("DOMContentLoaded", idempotent_fn, false);
8
        ...
9
    }
10
    //IE event model

11
    else if (document.attachEvent) {
12
13
        // ensure firing before onload; maybe late but safe also for iframes

14
        document.attachEvent("onreadystatechange", idempotent_fn);
15
16
        // A fallback to window.onload, that will always work

17
        window.attachEvent("onload", idempotent_fn);
18
19
        ...     
20
    }
21
22
})

Unter der Haube ist die ready() Methode gar nicht so einfach. jQuery normalisiert die Browser-Inkonsistenzen, um sicherzustellen, dass ready() zum richtigen Zeitpunkt ausgelöst wird. Als Entwickler erhalten Sie jedoch eine einfache Oberfläche.

Die meisten Beispiele des Fassadenmusters folgen diesem Prinzip. Wenn wir eines implementieren, verlassen wir uns normalerweise auf bedingte Anweisungen unter der Haube, aber präsentieren es als eine einfache Schnittstelle zum Benutzer. Andere Methoden, die dieses Muster implementieren, sind animate() und css(). Können Sie sich vorstellen, warum diese ein Fassadenmuster verwenden würden?


Verhaltensmuster

Jedes objektorientierte Softwaresystem wird eine Kommunikation zwischen Objekten haben. Das Nicht-Organisieren dieser Kommunikation kann zu Fehlern führen, die schwer zu finden und zu beheben sind. Verhaltensmuster schreiben verschiedene Methoden vor, um die Kommunikation zwischen Objekten zu organisieren. In diesem Abschnitt betrachten wir die Observer- und Mediator-Muster.

Beobachtermuster

Das Beobachtermuster ist das erste der beiden Verhaltensmuster, die wir durchlaufen werden. Hier ist, was es sagt:

Im Beobachtermuster kann ein Subjekt eine Liste von Beobachtern haben, die an seinem Lebenszyklus interessiert sind. Jedes Mal, wenn das Subjekt etwas Interessantes tut, sendet es eine Benachrichtigung an seine Beobachter. Wenn ein Beobachter nicht mehr an dem Thema interessiert ist, kann das Subjekt es aus seiner Liste entfernen.

Klingt ziemlich einfach, oder? Wir brauchen drei Methoden, um dieses Muster zu beschreiben:

  • publish(data): Wird vom Betreff aufgerufen, wenn eine Benachrichtigung vorliegt. Einige Daten können mit dieser Methode übergeben werden.
  • subscribe(observer): Wird vom Subjekt aufgerufen, um einen Beobachter zu seiner Liste von Beobachtern hinzuzufügen.
  • unsubscribe(observer): Wird vom Subjekt aufgerufen, um einen Beobachter aus seiner Beobachterliste zu entfernen.

Nun, es stellt sich heraus, dass die meisten modernen JavaScript-Bibliotheken diese drei Methoden als Teil ihrer benutzerdefinierten Ereignisinfrastruktur unterstützen. Normalerweise gibt es eine Methode on() oder attach(), eine Methode trigger() oder fire() und eine Methode off() oder detach(). Betrachten Sie das folgende Snippet:

1
//We just create an association between the jQuery events methods
1
//and those prescribed by the Observer Pattern but you don't have to.

2
var o = $( {} );
3
$.subscribe = o.on.bind(o);
4
$.unsubscribe = o.off.bind(o);
5
$.publish = o.trigger.bind(o);
6
7
// Usage

8
document.on( 'tweetsReceived', function(tweets) {
9
    //perform some actions, then fire an event

10
11
    $.publish('tweetsShow', tweets);
12
});
13
14
//We can subscribe to this event and then fire our own event.

15
$.subscribe( 'tweetsShow', function() {
16
    //display the tweets somehow

17
    ..
18
19
    //publish an action after they are shown.

20
    $.publish('tweetsDisplayed);

21
});

22


23
$.subscribe('tweetsDisplayed, function() {
24
    ...
25
});

Das Observer-Muster ist eines der einfacheren zu implementierenden Muster, aber es ist sehr leistungsfähig. JavaScript ist gut geeignet, um dieses Muster zu übernehmen, da es natürlich ereignisbasiert ist. Wenn Sie das nächste Mal Webanwendungen entwickeln, sollten Sie Module entwickeln, die lose miteinander gekoppelt sind und das Observer-Muster als Kommunikationsmittel verwenden. Das Beobachtermuster kann problematisch werden, wenn zu viele Personen und Beobachter beteiligt sind. Dies kann in großen Systemen passieren, und das nächste Muster, das wir betrachten, versucht dieses Problem zu lösen.

Mediator Muster

Das letzte Muster, das wir betrachten werden, ist das Mediator-Muster. Es ist ähnlich dem Beobachter-Muster, aber mit einigen bemerkenswerten Unterschieden.

Das Mediator-Muster fördert die Verwendung eines einzelnen gemeinsamen Subjekts, das die Kommunikation mit mehreren Objekten abwickelt. Alle Objekte kommunizieren miteinander über den Mediator.

Eine gute Analogie wäre ein Air Traffic Tower, der die Kommunikation zwischen dem Flughafen und den Flügen übernimmt. In der Welt der Softwareentwicklung wird das Mediator-Muster häufig verwendet, da ein System übermäßig kompliziert wird. Durch die Platzierung von Mediatoren kann die Kommunikation über ein einzelnes Objekt erfolgen, anstatt dass mehrere Objekte miteinander kommunizieren. In diesem Sinne kann ein Mediatormuster verwendet werden, um ein System zu ersetzen, das das Beobachtermuster implementiert.

Es gibt eine vereinfachte Implementierung des Mediator-Musters von Addy Osmani in diesem Geiste. Lass uns darüber reden, wie du es benutzen kannst. Stellen Sie sich vor, Sie haben eine Web-App, mit der Benutzer auf ein Album klicken und Musik von ihm abspielen können. Sie könnten einen Mediator wie folgt einrichten:

1
$('#album').on('click', function(e) {
2
    e.preventDefault();
3
    var albumId = $(this).id();
4
    mediator.publish("playAlbum", albumId);
5
});
6
7
8
var playAlbum = function(id) {
9
    
10
    mediator.publish("albumStartedPlaying", {songList: [..], currentSong: "Without You"});
11
12
};
13
14
var logAlbumPlayed = function(id) {
15
    //Log the album in the backend

16
};
17
18
var updateUserInterface = function(album) {
19
    //Update UI to reflect what's being played

20
};
21
22
//Mediator subscriptions

23
mediator.subscribe("playAlbum", playAlbum);
24
mediator.subscribe("playAlbum", logAlbumPlayed);
25
mediator.subscribe("albumStartedPlaying", updateUserInterface);

Der Vorteil dieses Musters gegenüber dem Observer-Muster besteht darin, dass ein einzelnes Objekt für die Kommunikation verantwortlich ist, wohingegen im Beobachtermuster mehrere Objekte einander zuhören und sich gegenseitig abonnieren könnten.

Im Observer-Muster gibt es kein einzelnes Objekt, das eine Einschränkung kapselt. Stattdessen müssen der Beobachter und das Subjekt zusammenarbeiten, um die Beschränkung aufrechtzuerhalten. Kommunikationsmuster werden durch die Art und Weise bestimmt, wie Beobachter und Subjekte miteinander verbunden sind: Ein einzelnes Subjekt hat normalerweise viele Beobachter, und manchmal ist der Beobachter eines Subjekts ein Subjekt eines anderen Beobachters.


Fazit

Jemand hat es in der Vergangenheit bereits erfolgreich angewendet.

Das Tolle an Design Patterns ist, dass jemand es in der Vergangenheit bereits erfolgreich angewendet hat. Es gibt viele Open-Source-Code, die verschiedene Muster in JavaScript implementieren. Als Entwickler müssen wir uns bewusst sein, welche Muster da sind und wann sie angewendet werden. Ich hoffe, dieses Tutorial hat Ihnen geholfen, einen Schritt weiter zu gehen, um diese Fragen zu beantworten.


Zusätzliches Lesen

Ein Großteil des Inhalts dieses Artikels findet sich im exzellenten Lern JavaScript Design Patterns-Buch von Addy Osmani. Es ist ein Online-Buch, das kostenlos unter einer Creative Commons-Lizenz veröffentlicht wurde. Das Buch behandelt ausführlich die Theorie und Implementierung vieler verschiedener Muster, sowohl in JavaScript als auch in verschiedenen JS-Bibliotheken. Ich ermutige Sie, es als Referenz anzusehen, wenn Sie Ihr nächstes Projekt beginnen.