Gibt es OO-Prinzipien, die für Javascript praktisch anwendbar sind?

79

Javascript ist eine prototypbasierte objektorientierte Sprache, kann jedoch auf verschiedene Arten klassenbasiert werden:

  • Schreiben Sie die Funktionen, die Sie als Klassen verwenden möchten
  • Verwenden Sie ein intelligentes Klassensystem in einem Framework (z. B. mootools Class.Class ).
  • Erzeugen Sie es aus Coffeescript

Am Anfang habe ich dazu tendiert, klassenbasierten Code in Javascript zu schreiben und mich stark darauf verlassen. In letzter Zeit habe ich jedoch Javascript-Frameworks und NodeJS verwendet , die von diesem Klassenbegriff abweichen und sich mehr auf die Dynamik des Codes stützen, wie zum Beispiel:

  • Asynchrone Programmierung, Verwenden und Schreiben von Code, der Rückrufe / Ereignisse verwendet
  • Laden von Modulen mit RequireJS (damit sie nicht in den globalen Namespace gelangen)
  • Funktionale Programmierkonzepte wie Listenverständnis (Karte, Filter etc.)
  • Unter anderem

Was ich bisher gesammelt habe, ist, dass die meisten OO-Prinzipien und -Muster, die ich gelesen habe (wie SOLID- und GoF-Muster), für klassenbasierte OO-Sprachen wie Smalltalk und C ++ geschrieben wurden. Aber gibt es welche, die für eine prototypbasierte Sprache wie Javascript geeignet sind?

Gibt es Prinzipien oder Muster, die nur für Javascript spezifisch sind? Grundsätze zur Vermeidung von Callback-Hölle , Eval oder anderen Anti-Mustern usw.

Spoike
quelle

Antworten:

116

Nach vielen Änderungen hat sich diese Antwort in der Länge zu einem Monster entwickelt. Ich entschuldige mich im Voraus.

Erstens eval()ist es nicht immer schlecht und kann sich positiv auf die Leistung auswirken, wenn es zum Beispiel in der Lazy-Evaluation eingesetzt wird. Lazy-Evaluation ähnelt dem Lazy-Loading, aber Sie speichern Ihren Code im Wesentlichen in Strings und verwenden evaloder new Function, um den Code zu evaluieren. Wenn Sie einige Tricks anwenden, wird es viel nützlicher als das Böse, aber wenn Sie es nicht tun, kann es zu schlechten Dingen führen. Sie können sich mein Modulsystem ansehen, das dieses Muster verwendet: https://github.com/TheHydroImpulse/resolve.js . Resolve.js verwendet eval, anstatt in new Functionerster Linie das CommonJS exportsund die moduleVariablen zu modellieren, die in jedem Modul verfügbar sind, und new Functionumschließt Ihren Code mit einer anonymen Funktion. Am Ende verpacke ich jedoch jedes Modul in eine Funktion, die ich manuell in Kombination mit eval durchführe.

In den folgenden beiden Artikeln lesen Sie mehr darüber, wobei sich der spätere auch auf den ersten bezieht.

Harmony-Generatoren

Jetzt, da Generatoren endlich in V8 und damit in Node.js gelandet sind, unter einer Flagge ( --harmonyoder --harmony-generators). Dies reduziert die Anzahl der Rückrufe erheblich. Es macht das Schreiben von asynchronem Code wirklich großartig.

Der beste Weg, um Generatoren zu nutzen, besteht darin, eine Art Kontrollflussbibliothek zu verwenden. Dadurch kann der Fluss fortgesetzt werden, während Sie innerhalb der Generatoren nachgeben.

Rückblick / Übersicht:

Wenn Sie mit Generatoren nicht vertraut sind, unterbrechen Sie die Ausführung spezieller Funktionen (sogenannte Generatoren). Diese Praxis nennt man Nachgeben mit dem yieldSchlüsselwort.

Beispiel:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}

Wenn Sie diese Funktion also zum ersten Mal aufrufen, wird eine neue Generatorinstanz zurückgegeben. Auf diese Weise können Sie next()dieses Objekt aufrufen , um den Generator zu starten oder fortzusetzen.

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }

Sie würden so lange telefonieren, nextbis Sie donezurückkehren true. Dies bedeutet, dass der Generator seine Ausführung vollständig abgeschlossen hat und keine weiteren yieldAnweisungen vorliegen.

Kontrollfluss:

Wie Sie sehen, erfolgt die Steuerung von Generatoren nicht automatisch. Sie müssen jedes manuell fortsetzen. Deshalb werden Kontrollflussbibliotheken wie co verwendet.

Beispiel:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});

Dies ermöglicht die Möglichkeit, alles in Node (und den Browser mit dem Regenerator von Facebook , der als Eingabe Quellcode verwendet, der Harmony-Generatoren verwendet und voll kompatiblen ES5-Code aufteilt) synchron zu schreiben.

Generatoren sind noch ziemlich neu und erfordern daher Node.js> = v11.2. Während ich dies schreibe, ist v0.11.x immer noch instabil und daher sind viele native Module defekt und werden es bis v0.12 sein, wo sich die native API beruhigen wird.


Zu meiner ursprünglichen Antwort hinzufügen:

Ich bevorzuge kürzlich eine funktionalere API in JavaScript. Die Konvention verwendet OOP im Hintergrund, wenn dies erforderlich ist, vereinfacht jedoch alles.

Nehmen wir zum Beispiel ein View-System (Client oder Server).

view('home.welcome');

Ist viel einfacher zu lesen oder zu befolgen als:

var views = {};
views['home.welcome'] = new View('home.welcome');

Die viewFunktion prüft einfach, ob dieselbe Ansicht bereits in einer lokalen Karte vorhanden ist. Wenn die Ansicht nicht vorhanden ist, wird eine neue Ansicht erstellt und der Karte ein neuer Eintrag hinzugefügt.

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};

Sehr einfach, oder? Ich finde, dass es die öffentliche Benutzeroberfläche dramatisch vereinfacht und die Verwendung erleichtert. Ich benutze auch Kettenfähigkeit ...

view('home.welcome')
   .child('menus')
   .child('auth')

Tower, ein Framework, das ich (mit jemand anderem) oder die nächste Version (0.5.0) entwickle, wird diesen funktionalen Ansatz in den meisten seiner offengelegten Schnittstellen verwenden.

Einige Leute nutzen Fasern, um eine "Rückruf-Hölle" zu vermeiden. Es ist eine ganz andere Herangehensweise an JavaScript, und ich bin kein großer Fan davon, aber viele Frameworks / Plattformen verwenden es. einschließlich Meteor, da sie Node.js als Thread / pro Verbindungsplattform behandeln.

Ich würde lieber eine abstrahierte Methode verwenden, um eine Rückruf-Hölle zu vermeiden. Es kann umständlich werden, aber es vereinfacht den eigentlichen Anwendungscode erheblich. Bei der Erstellung des TowerJS- Frameworks konnten viele unserer Probleme behoben werden. Sie haben jedoch offensichtlich immer noch einige Rückrufe, aber die Verschachtelung ist nicht tiefgreifend.

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});

Ein Beispiel für unser derzeit entwickeltes Routing-System und unsere "Controller", die sich jedoch von den herkömmlichen "rail-like" unterscheiden. Aber das Beispiel ist extrem leistungsfähig und minimiert die Anzahl der Rückrufe und macht die Dinge ziemlich offensichtlich.

Das Problem bei diesem Ansatz ist, dass alles abstrahiert ist. Nichts läuft so wie es ist und erfordert ein "Framework" dahinter. Wenn diese Art von Funktionen und Codierungsstil jedoch in einem Framework implementiert wird, ist dies ein enormer Gewinn.

Bei Mustern in JavaScript kommt es ehrlich gesagt darauf an. Vererbung ist nur dann wirklich nützlich, wenn Sie CoffeeScript, Ember oder ein "Klassen" -Framework / eine "Klassen" -Infrastruktur verwenden. Wenn Sie sich in einer "reinen" JavaScript-Umgebung befinden, funktioniert die Verwendung der traditionellen Prototyp-Oberfläche wie ein Zauber:

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};

Zumindest für mich begann Ember.js mit einer anderen Herangehensweise beim Konstruieren von Objekten. Anstatt die einzelnen Prototypmethoden unabhängig voneinander zu erstellen, verwenden Sie eine modulartige Schnittstelle.

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});

All dies sind verschiedene "Codierungs" -Stile, die jedoch zu Ihrer Codebasis beitragen.

Polymorphismus

Der Polymorphismus wird in reinem JavaScript nicht häufig verwendet, da für die Arbeit mit der Vererbung und das Kopieren des "klassen" -ähnlichen Modells viel Code erforderlich ist.

Ereignis- / komponentenbasiertes Design

Ereignis- und komponentenbasierte Modelle sind die Gewinner der IMO oder die am einfachsten zu bearbeitenden Modelle, insbesondere bei der Arbeit mit Node.js, das über eine integrierte EventEmitter-Komponente verfügt. Die Implementierung solcher Emitter ist jedoch trivial, eine nette Ergänzung .

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});

Nur ein Beispiel, aber es ist ein schönes Modell, mit dem man arbeiten kann. Besonders in einem spiel- / komponentenorientierten Projekt.

Das Komponentendesign ist ein eigenständiges Konzept, aber ich denke, es funktioniert hervorragend in Kombination mit Ereignissystemen. Spiele sind traditionell für komponentenbasiertes Design bekannt, bei dem die objektorientierte Programmierung nur so weit führt.

Das komponentenbasierte Design hat seine Verwendung. Es hängt davon ab, welche Art von System Ihr Gebäude hat. Ich bin mir sicher, dass es mit Web-Apps funktionieren würde, aber es würde in einer Spielumgebung aufgrund der Anzahl von Objekten und separaten Systemen sehr gut funktionieren, aber es gibt sicherlich andere Beispiele.

Pub / Sub-Muster

Event-Binding und Pub / Sub sind ähnlich. Das Pub- / Sub-Muster glänzt in Node.js-Anwendungen aufgrund der einheitlichen Sprache, kann jedoch in jeder Sprache verwendet werden. Funktioniert hervorragend in Echtzeitanwendungen, Spielen usw.

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});

Beobachter

Dies kann subjektiv sein, da einige Leute das Observer-Muster als Pub / Sub betrachten, aber sie haben ihre Unterschiede.

"Der Beobachter ist ein Entwurfsmuster, bei dem ein Objekt (bekannt als Subjekt) eine Liste von Objekten verwaltet, die von ihm abhängen (Beobachter), und diese automatisch über Statusänderungen benachrichtigt." - Das Beobachtermuster

Das Beobachtermuster ist ein Schritt über typische Pub / Sub-Systeme hinaus. Objekte haben strenge Beziehungen oder Kommunikationsmethoden miteinander. Ein Objekt "Betreff" würde eine Liste von abhängigen "Beobachtern" führen. Das Thema würde seine Beobachter auf dem Laufenden halten.

Reaktive Programmierung

Reaktive Programmierung ist ein kleineres, unbekannteres Konzept, insbesondere in JavaScript. Es gibt ein Framework / eine Bibliothek (die ich kenne), die eine einfach zu handhabende API zur Verwendung dieser "reaktiven Programmierung" bereitstellt.

Ressourcen zur reaktiven Programmierung:

Im Grunde handelt es sich um eine Reihe von Synchronisierungsdaten (seien es Variablen, Funktionen usw.).

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4

Ich glaube, dass reaktive Programmierung, insbesondere in imperativen Sprachen, stark verborgen ist. Es ist ein erstaunlich leistungsfähiges Programmierparadigma, insbesondere in Node.js. Meteor hat eine eigene reaktive Engine entwickelt, auf der das Framework basiert. Wie funktioniert Meteors Reaktivität hinter den Kulissen? gibt einen guten Überblick darüber, wie es intern funktioniert.

Meteor.autosubscribe(function() {
   console.log("Hello " + Session.get("name"));
});

Dies wird normal ausgeführt und zeigt den Wert von an name, aber wenn wir ihn ändern

Session.set ('name', 'Bob');

Das angezeigte console.log wird erneut ausgegeben Hello Bob. Ein einfaches Beispiel, aber Sie können diese Technik auf Echtzeitdatenmodelle und -transaktionen anwenden. Sie können extrem leistungsfähige Systeme hinter diesem Protokoll erstellen.

Meteor ...

Reaktivmuster und Beobachtermuster sind sehr ähnlich. Der Hauptunterschied besteht darin, dass das Beobachtermuster häufig den Datenfluss mit ganzen Objekten / Klassen beschreibt, während reaktive Programmierung den Datenfluss zu bestimmten Eigenschaften beschreibt.

Meteor ist ein großartiges Beispiel für reaktive Programmierung. Die Laufzeit ist etwas kompliziert, da JavaScript keine nativen Werteänderungsereignisse aufweist (dies wird durch Harmony-Proxys geändert). Andere clientseitige Frameworks, Ember.js und AngularJS, verwenden ebenfalls reaktive Programmierung (in gewissem Umfang).

Die beiden späteren Frameworks verwenden das reaktive Muster vor allem in ihren Vorlagen (dh automatische Aktualisierung). Angular.js verwendet eine einfache schmutzige Prüftechnik. Ich würde das nicht als genau reaktive Programmierung bezeichnen, aber es ist nah, da Dirty Checking nicht in Echtzeit erfolgt. Ember.js verwendet einen anderen Ansatz. Verwendung set()und get()Methoden von Ember, mit denen sie abhängige Werte sofort aktualisieren können. Mit ihrer Runloop ist sie äußerst effizient und ermöglicht abhängigere Werte, bei denen der Winkel eine theoretische Grenze hat.

Versprechen

Keine Behebung von Rückrufen, sondern Herausnehmen von Einrückungen und Minimieren der verschachtelten Funktionen. Es fügt dem Problem auch eine nette Syntax hinzu.

fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
  return fs.read(fd, 4096);
}).then(function(args){
  util.puts(args[0]); // print the contents of the file
});

Sie können die Rückruffunktionen auch so verteilen, dass sie nicht inline sind, aber das ist eine andere Entwurfsentscheidung.

Ein anderer Ansatz wäre, Ereignisse und Versprechungen dahingehend zu kombinieren, dass Sie die Funktion haben, Ereignisse entsprechend zu senden, und dann die tatsächlichen Funktionsfunktionen (die die eigentliche Logik enthalten) an ein bestimmtes Ereignis zu binden. Sie würden dann die Dispatcher-Methode in jeder Callback-Position übergeben. Allerdings müssten Sie einige Probleme herausfinden, die Ihnen in den Sinn kämen, z. B. Parameter, das Wissen, an welche Funktion Sie senden sollen usw.

Einzelfunktion Funktion

Anstatt ein riesiges Durcheinander von Rückrufen zu haben, behalten Sie eine einzelne Funktion für eine einzelne Aufgabe bei und erledigen Sie diese Aufgabe gut. Manchmal können Sie sich selbst übertreffen und jeder Funktion mehr Funktionen hinzufügen. Fragen Sie sich jedoch: Kann dies eine eigenständige Funktion werden? Nennen Sie die Funktion, und dies bereinigt Ihre Einrückung und als Ergebnis bereinigt das Problem der Rückrufhölle.

Am Ende würde ich vorschlagen, ein kleines "Framework" zu entwickeln oder zu verwenden, das im Grunde nur ein Rückgrat für Ihre Anwendung ist, und mir Zeit nehmen, um Abstraktionen vorzunehmen, ein ereignisbasiertes System zu wählen oder "viele kleine Module zu verwenden, die es sind" unabhängiges "System. Ich habe mit mehreren Node.js-Projekten gearbeitet, bei denen der Code vor allem wegen der Rückruf-Hölle extrem chaotisch war, aber auch mangelnde Überlegungen, bevor sie mit dem Codieren begannen. Nehmen Sie sich Zeit, um die verschiedenen Möglichkeiten in Bezug auf API und Syntax zu durchdenken.

Ben Nadel hat einige wirklich gute Blog-Posts über JavaScript und einige ziemlich strenge und fortgeschrittene Muster verfasst, die in Ihrer Situation funktionieren können. Einige gute Beiträge, die ich hervorheben werde:

Umkehrung der Kontrolle

Obwohl es nicht gerade mit der Callback-Hölle zu tun hat, kann es Ihnen bei der Gesamtarchitektur helfen, insbesondere bei den Unit-Tests.

Die beiden Hauptunterversionen von Inversion-of-Control sind Dependency Injection und Service Locator. Ich finde, dass der Service Locator in JavaScript im Gegensatz zur Abhängigkeitsinjektion am einfachsten ist. Warum? Hauptsächlich, weil JavaScript eine dynamische Sprache ist und keine statische Typisierung existiert. Java und C # sind unter anderem für die Abhängigkeitsinjektion "bekannt", da sie Typen erkennen können und Schnittstellen, Klassen usw. eingebaut haben. Dies macht die Sache ziemlich einfach. Sie können diese Funktionalität jedoch in JavaScript neu erstellen. Sie wird jedoch nicht identisch sein und ist ein bisschen kitschig. Ich bevorzuge die Verwendung eines Service-Locators in meinen Systemen.

Jede Art von Inversion-of-Control entkoppelt Ihren Code dramatisch in separate Module, die jederzeit verspottet oder gefälscht werden können. Entwickelt eine zweite Version Ihrer Rendering-Engine? Genial, ersetze einfach das alte Interface durch das neue. Service-Locators sind besonders interessant für die neuen Harmony-Proxies, die jedoch nur in Node.js effektiv verwendet werden können. Sie bieten eine schönere API, anstatt Service.get('render');und zu verwenden Service.render. Ich arbeite derzeit an dieser Art von System: https://github.com/TheHydroImpulse/Ettore .

Obwohl das Fehlen der statischen Typisierung (statische Typisierung ist ein möglicher Grund für die effektive Verwendung von Dependency Injection in Java, C # und PHP - Es ist nicht statisch typisiert, hat aber Tippfehler) möglicherweise als negativer Punkt angesehen wird, können Sie dies tun Machen Sie es auf jeden Fall zu einem starken Punkt. Da alles dynamisch ist, können Sie ein "falsches" statisches System entwickeln. In Kombination mit einem Service-Locator kann jede Komponente / Modul / Klasse / Instanz an einen Typ gebunden sein.

var Service, componentA;

function Manager() {
  this.instances = {};
}

Manager.prototype.get = function(name) {
  return this.instances[name];
};

Manager.prototype.set = function(name, value) {
  this.instances[name] = value;
};

Service = new Manager();
componentA = {
  type: "ship",
  value: new Ship()
};

Service.set('componentA', componentA);

// DI
function World(ship) {
  if (ship === Service.matchType('ship', ship))
    this.ship = new ship();
  else
    throw Error("Wrong type passed.");
}

// Use Case:
var worldInstance = new World(Service.get('componentA'));

Ein vereinfachtes Beispiel. Für eine effektive Nutzung in der realen Welt müssen Sie dieses Konzept weiterentwickeln. Es kann jedoch hilfreich sein, Ihr System zu entkoppeln, wenn Sie wirklich eine herkömmliche Abhängigkeitsinjektion wünschen. Möglicherweise müssen Sie sich ein wenig mit diesem Konzept beschäftigen. Ich habe nicht viel über das vorherige Beispiel nachgedacht.

Model View Controller

Das offensichtlichste und am häufigsten im Web verwendete Muster. Vor ein paar Jahren war JQuery der letzte Schrei und so wurden JQuery-Plugins geboren. Auf der Client-Seite war kein vollständiges Framework erforderlich. Verwenden Sie einfach jquery und einige Plugins.

Jetzt gibt es einen großen Krieg um das clientseitige JavaScript-Framework. Die meisten von ihnen verwenden das MVC-Muster und alle verwenden es unterschiedlich. MVC ist nicht immer gleich implementiert.

Wenn Sie die traditionellen prototypischen Schnittstellen verwenden, fällt es Ihnen möglicherweise schwer, einen syntaktischen Zucker oder eine nette API zu erhalten, wenn Sie mit MVC arbeiten, es sei denn, Sie möchten manuell arbeiten. Ember.js löst dieses Problem, indem ein "Klassen" / Objekt "-System erstellt wird. Ein Controller könnte folgendermaßen aussehen:

 var Controller = Ember.Controller.extend({
      index: function() {
        // Do something....
      }
 });

Die meisten clientseitigen Bibliotheken erweitern das MVC-Muster auch durch die Einführung von Ansichtshilfen (werden zu Ansichten) und Vorlagen (werden zu Ansichten).


Neue JavaScript-Funktionen:

Dies ist nur dann effektiv, wenn Sie Node.js verwenden, es ist jedoch von unschätzbarem Wert. Dieser Vortrag auf der NodeConf von Brendan Eich bringt einige coole neue Funktionen. Die vorgeschlagene Funktionssyntax und insbesondere die Bibliothek Task.js js.

Dies wird wahrscheinlich die meisten Probleme mit der Verschachtelung von Funktionen beheben und aufgrund des fehlenden Funktionsaufwands eine etwas bessere Leistung bringen.

Ich bin mir nicht sicher, ob V8 dies nativ unterstützt. Zuletzt habe ich überprüft, ob Sie einige Flags aktivieren müssen, aber dies funktioniert in einem Port von Node.js, der SpiderMonkey verwendet .

Zusätzliche Ressourcen:

Daniel
quelle
2
Nizza schreiben. Ich persönlich habe keine Verwendung für die MV? Bibliotheken. Wir haben alles, was wir brauchen, um unseren Code für größere, komplexere Apps zu organisieren. Sie alle erinnern mich zu sehr an Java und C #, die versuchen, ihre eigenen Vorhänge über das zu werfen, was in der Server-Client-Kommunikation tatsächlich vor sich ging. Wir haben einen DOM. Wir haben eine Event-Delegation. Wir haben OOP. Ich kann meine eigenen Ereignisse an Datenänderungen binden.
Erik Reppen
2
"Anstatt ein riesiges Durcheinander von Callback-Höllen zu haben, behalten Sie eine einzelne Funktion für eine einzelne Aufgabe bei und erledigen Sie diese Aufgabe gut." - Poesie.
CuriousWebDeveloper
1
Javascript in einem sehr dunklen Zeitalter in den frühen bis mittleren 2000er Jahren, als nur wenige verstanden, wie man damit große Anwendungen schreibt. Wie @ErikReppen sagt, wenn Sie feststellen, dass Ihre JS-Anwendung wie eine Java- oder C # -Anwendung aussieht, machen Sie es falsch.
Backpackcoder
3

Hinzufügen zu Daniels Antwort:

Beobachtbare Werte / Komponenten

Diese Idee ist dem MVVM-Framework Knockout.JS ( ko.observable ) entlehnt , mit der Idee, dass Werte und Objekte beobachtbare Subjekte sein können. Sobald sich ein Wert oder ein Objekt ändert, werden alle Beobachter automatisch aktualisiert. Es ist im Grunde das in Javascript implementierte Beobachtermuster, und stattdessen, wie die meisten Pub / Sub-Frameworks implementiert sind, ist der "Schlüssel" das Subjekt selbst anstelle eines beliebigen Objekts.

Die Verwendung ist wie folgt:

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);

Die Idee ist, dass Beobachter normalerweise wissen, wo sich das Thema befindet und wie sie es abonnieren. Der Vorteil davon anstelle des Pubs / Sub ist bemerkbar, wenn Sie den Code viel ändern müssen, da es einfacher ist, Themen als Refactoring-Schritt zu entfernen. Ich meine das, weil, sobald Sie ein Thema entfernen, jeder, der davon abhängig war, scheitert. Wenn der Code schnell fehlschlägt, wissen Sie, wo Sie die verbleibenden Verweise entfernen müssen. Dies steht im Gegensatz zu dem vollständig entkoppelten Betreff (z. B. mit einem String-Schlüssel in einem Pub / Sub-Muster) und hat eine höhere Wahrscheinlichkeit, im Code zu bleiben, insbesondere wenn dynamische Schlüssel verwendet wurden und der Wartungsprogrammierer nicht darauf aufmerksam gemacht wurde (tot) Code in der Wartungsprogrammierung ist ein ärgerliches Problem.

Im Spiele - Programmierung, reduziert dies die Notwendigkeit von ye olde Update Schleifenmuster und mehr in eine evented / reaktive Programmierung Idiom, denn sobald etwas das Thema automatisch geändert wird , werden alle Beobachter auf der Änderung aktualisieren, ohne auf die Aktualisierungsschleife zu warten, ausführen. Es gibt Verwendungen für die Aktualisierungsschleife (für Dinge, die mit der abgelaufenen Spielzeit synchronisiert werden müssen), aber manchmal möchten Sie es einfach nicht überladen, wenn sich die Komponenten selbst mit diesem Muster automatisch aktualisieren können.

Die eigentliche Implementierung der Observable-Funktion ist überraschend einfach zu schreiben und zu verstehen (insbesondere, wenn Sie wissen, wie man mit Arrays in Javascript und dem Observer-Pattern umgeht ):

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};

Ich habe gemacht eine Implementierung des beobachtbaren Objekts in JsFiddle erstellt , die darauf aufbaut , Komponenten zu beobachten und Abonnenten entfernen zu können. Fühlen Sie sich frei, die JsFiddle zu experimentieren.

Spoike
quelle