it-swarm.com.de

Ist es eine schlechte Praxis, wenn eine Konstruktorfunktion ein Versprechen zurückgibt?

Ich versuche, einen Konstruktor für eine Blogging-Plattform zu erstellen, in dem viele asynchrone Operationen ausgeführt werden. Dazu gehören das Abrufen der Beiträge aus Verzeichnissen, das Analysieren, das Senden durch Vorlagenmodule usw.

Meine Frage ist also, ob es unklug wäre, wenn meine Konstruktorfunktion anstelle eines Objekts der Funktion, gegen die sie new gerufen wurde, ein Versprechen zurückgibt.

Zum Beispiel:

var engine = new Engine({path: '/path/to/posts'}).then(function (eng) {
   // allow user to interact with the newly created engine object inside 'then'
   engine.showPostsOnOnePage();
});

Nun kann der Benutzer auch nicht eine ergänzende Promise-Kettenverbindung bereitstellen:

var engine = new Engine({path: '/path/to/posts'});

// ERROR
// engine will not be available as an Engine object here

Dies könnte ein Problem darstellen, da der Benutzer verwirrt sein kann, warumenginenach der Erstellung nicht verfügbar ist.

Der Grund für die Verwendung eines Versprechens im Konstruktor ist sinnvoll. Ich möchte, dass der gesamte Blog nach der Bauphase funktioniert. Es scheint jedoch fast ein Geruch zu sein, dass er unmittelbar nach dem Aufruf von new keinen Zugriff auf das Objekt hat.

Ich habe darüber nachgedacht, etwas in der Art von engine.start().then() oder engine.init() zu verwenden, das stattdessen das Versprechen zurückgeben würde. Diese scheinen aber auch zu stinken.

Bearbeiten: Dies ist in einem Node.js-Projekt.

129
adam-beck

Ja, das ist eine schlechte Praxis. Ein Konstruktor sollte eine Instanz seiner Klasse zurückgeben, sonst nichts. Andernfalls würde der Operator new und die Vererbung durcheinander gebracht.

Darüber hinaus sollte ein Konstruktor nur eine neue Instanz erstellen und initialisieren. Es sollte Datenstrukturen und alle instanzspezifischen Eigenschaften einrichten, aber keine Aufgaben ausführen . Es sollte ein reine Funktion ohne Nebenwirkungen sein, wenn möglich, mit allen Vorteilen, die das hat.

Was ist, wenn ich Dinge von meinem Konstruktor ausführen möchte?

Das sollte in eine Methode Ihrer Klasse passen. Sie wollen den globalen Staat mutieren lassen? Rufen Sie diese Prozedur dann explizit auf, nicht als Nebeneffekt beim Generieren eines Objekts. Dieser Aufruf kann direkt nach der Instanziierung erfolgen:

var engine = new Engine()
engine.displayPosts();

Wenn diese Aufgabe asynchron ist, können Sie jetzt auf einfache Weise eine Zusage für die Ergebnisse der Methode zurückgeben und warten, bis sie abgeschlossen ist.
Ich würde dieses Muster jedoch nicht empfehlen, wenn die Methode (asynchron) die Instanz mutiert und andere Methoden davon abhängen, da dies dazu führen würde, dass sie warten müssen (asynchron werden, auch wenn sie tatsächlich synchron sind) und Sie müssten schnell eine interne Warteschlangenverwaltung durchführen. Codieren Sie keine Instanzen, um zu existieren, sondern um tatsächlich unbrauchbar zu sein.

Was ist, wenn ich Daten asynchron in meine Instanz laden möchte?

Fragen Sie sich: Benötigen Sie die Instanz tatsächlich ohne die Daten? Könnten Sie es irgendwie benutzen?

Wenn die Antwort darauf Nein ist, sollten Sie es nicht erstellen, bevor Sie die Daten haben. Machen Sie die Daten selbst zu einem Parameter für Ihren Konstruktor, anstatt dem Konstruktor mitzuteilen, wie die Daten abgerufen werden sollen (oder ein Versprechen für die Daten abzugeben).

Verwenden Sie dann eine statische Methode, um die Daten zu laden, von denen Sie ein Versprechen zurückgeben. Verketten Sie dann einen Aufruf, der die Daten in eine neue Instanz einschließt:

Engine.load({path: '/path/to/posts'}).then(function(posts) {
    new Engine(posts).displayPosts();
});

Dies ermöglicht eine viel größere Flexibilität bei der Erfassung der Daten und vereinfacht den Konstruktor erheblich. In ähnlicher Weise können Sie statische Factory-Funktionen schreiben, die Versprechungen für Engine -Instanzen zurückgeben:

Engine.fromPosts = function(options) {
    return ajax(options.path).then(Engine.parsePosts).then(function(posts) {
        return new Engine(posts, options);
    });
};

…

Engine.fromPosts({path: '/path/to/posts'}).then(function(engine) {
    engine.registerWith(framework).then(function(framePage) {
        engine.showPostsOn(framePage);
    });
});
183
Bergi

Ich bin auf das gleiche Problem gestoßen und habe diese einfache Lösung gefunden.

Anstatt ein Promise vom Konstruktor zurückzugeben, geben Sie es in die this.initialization-Eigenschaft ein:

function Engine(path) {
  var engine = this
  engine.initialization = Promise.resolve()
    .then(function () {
      return doSomethingAsync(path)
    })
    .then(function (result) {
      engine.resultOfAsyncOp = result
    })
}

Wickeln Sie dann jede Methode in einen Rückruf ein, der nach der Initialisierung wie folgt ausgeführt wird:

Engine.prototype.showPostsOnPage = function () {
  return this.initialization.then(function () {
    // actual body of the method
  })
}

So sieht es aus der API-Consumer-Perspektive aus:

engine = new Engine({path: '/path/to/posts'})
engine.showPostsOnPage()

Dies funktioniert, weil Sie mehrere Rückrufe für ein Versprechen registrieren können und diese entweder ausgeführt werden, nachdem sie aufgelöst wurden, oder, falls bereits gelöst, zum Zeitpunkt des Anbringens des Rückrufs.

So funktioniert mongoskin , es werden jedoch keine Versprechungen verwendet.


Edit: Seit ich diese Antwort geschrieben habe, habe ich mich in die ES6/7-Syntax verliebt, also gibt es noch ein anderes Beispiel. Sie können es heute mit Babel verwenden.

class Engine {

  constructor(path) {
    this._initialized = this._initialize()
  }

  async _initialize() {
    // actual async constructor logic
  }

  async showPostsOnPage() {
    await this._initialized
    // actual body of the method
  }

}

Edit: Sie können dieses Muster nativ mit Knoten 7 und --harmony verwenden.

10
phaux

Um die Trennung von Bedenken zu vermeiden, erstellen Sie das Objekt in einer Fabrik. 

class Engine {
    constructor(data) {
        this.data = data;
    }

    static makeEngine(pathToData) {
        return new Promise((resolve, reject) => {
            getData(pathToData).then(data => {
              resolve(new Engine(data))
            }).catch(reject);
        });
    }
}
3
The Farmer

Der Rückgabewert des Konstruktors ersetzt das Objekt, das der neue Operator gerade erzeugt hat. Die Rückgabe eines Versprechens ist daher keine gute Idee. Bisher wurde ein expliziter Rückgabewert vom Konstruktor für das Singleton-Muster verwendet.

Der bessere Weg in ECMAScript 2017 ist die Verwendung statischer Methoden: Sie haben einen Prozess, nämlich die Anzahl der statischen.

Welche Methode für das neue Objekt nach dem Konstruktor ausgeführt wird, ist möglicherweise nur der Klasse selbst bekannt. Um dies in der Klasse einzukapseln, können Sie process.nextTick oder Promise.resolve verwenden, um die weitere Ausführung zu verschieben, sodass Listener und andere Elemente in Process.launch, dem Aufrufer des Konstruktors, hinzugefügt werden können.

Da fast der gesamte Code in einem Promise ausgeführt wird, landen Fehler in Process.fatal

Diese Grundidee kann modifiziert werden, um den spezifischen Anforderungen der Kapselung zu entsprechen.

class MyClass {
  constructor(o) {
    if (o == null) o = false
    if (o.run) Promise.resolve()
      .then(() => this.method())
      .then(o.exit).catch(o.reject)
  }

  async method() {}
}

class Process {
  static launch(construct) {
    return new Promise(r => r(
      new construct({run: true, exit: Process.exit, reject: Process.fatal})
    )).catch(Process.fatal)
  }

  static exit() {
    process.exit()
  }

  static fatal(e) {
    console.error(e.message)
    process.exit(1)
  }
}

Process.launch(MyClass)
0
Harald Rudell