web-dev-qa-db-ja.com

コンストラクター関数にPromiseを返すのは悪い習慣ですか?

ブログプラットフォーム用のコンストラクタを作成しようとしていますが、内部で多くの非同期操作が行われています。これらは、ディレクトリから投稿を取得すること、それらを解析すること、テンプレートエンジンを介して送信することなどに及びます。

したがって、私の質問は、コンストラクタ関数がnewに対して呼び出した関数のオブジェクトの代わりにpromiseを返すのは賢明ではないでしょうか。

例えば:

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

現在、ユーザーはnot補足のPromiseチェーンリンクを提供することもできます。

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

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

これは、ユーザーが混乱する可能性があるため、問題を引き起こす可能性がありますengineは構築後に利用できません。

コンストラクターでPromiseを使用する理由は理にかなっています。私は、ブログ全体が建設段階後に機能することを望んでいます。ただし、newを呼び出した直後にオブジェクトにアクセスできないことは、ほとんど臭いのようです。

代わりにPromiseを返すengine.start().then()またはengine.init()の行に沿って何かを使用して議論しました。しかし、それらも臭いのようです。

編集:これはNode.jsプロジェクトにあります。

144
adam-beck

はい、それは悪い練習です。コンストラクターは、クラスのインスタンスを返すだけで、それ以外は何も返しません。 new演算子 とそれ以外の継承を台無しにします。

さらに、コンストラクターは新しいインスタンスを作成および初期化するだけです。データ構造とすべてのインスタンス固有のプロパティを設定する必要がありますが、タスクはexecuteではありません。 純粋な関数 であり、可能であれば副作用はなく、すべての利点があります。

コンストラクターから実行したい場合はどうすればよいですか?

それはあなたのクラスのメソッドに入れるべきです。グローバル状態を変更したいですか?次に、オブジェクトを生成する副作用としてではなく、そのプロシージャを明示的に呼び出します。この呼び出しは、インスタンス化の直後に実行できます。

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

そのタスクが非同期の場合、メソッドから結果のプロミスを簡単に返して、完了するまで簡単に待つことができます。
ただし、メソッドが(非同期で)インスタンスを変更し、他のメソッドがそれに依存している場合は、このパターンをお勧めしません。内部キュー管理をすぐに実行できます。存在するようにインスタンスをコーディングしないでください。実際には使用できません。

インスタンスに非同期にデータをロードする場合はどうなりますか?

自問してください:データのないインスタンスが実際に必要ですか?どうにか使えますか?

その答えがNoである場合、データを取得する前に作成しないでください。データをフェッチする方法をコンストラクターに指示する(またはデータのプロミスを渡す)代わりに、データifselfをコンストラクターのパラメーターにします。

次に、静的メソッドを使用してデータをロードし、そこからプロミスを返します。次に、その上に新しいインスタンスでデータをラップする呼び出しをチェーンします。

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

これにより、データを取得する方法の柔軟性が大幅に向上し、コンストラクターが大幅に簡素化されます。同様に、Engineインスタンスのpromiseを返す静的ファクトリー関数を作成できます。

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

私は同じ問題に遭遇し、この簡単な解決策を思いつきました。

コンストラクターからPromiseを返す代わりに、次のようにthis.initializationプロパティに入れます。

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

次に、次のように、初期化後に実行されるコールバックですべてのメソッドをラップします。

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

APIコンシューマーの観点から見た場合:

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

これは、複数のコールバックをプロミスに登録でき、それらが解決された後に実行されるか、すでに解決されている場合はコールバックをアタッチするときに実行されるためです。

これが mongoskin の仕組みですが、実際にはプロミスを使用しません。


編集:その返信を書いてから、ES6/7構文に夢中になったので、それを使用した別の例があります。今日はbabelで使用できます。

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:このパターンは、ノード7および--harmonyフラグでネイティブに使用できます!

12
phaux

懸念の分離を回避するには、ファクトリを使用してオブジェクトを作成します。

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

コンストラクタからの戻り値は、new演算子が生成したばかりのオブジェクトを置き換えるため、promiseを返すことはお勧めできません。以前は、コンストラクターからの明示的な戻り値がシングルトンパターンに使用されていました。

ECMAScript 2017でのより良い方法は、静的メソッドを使用することです。1つのプロセスがあり、それが静的の数です。

コンストラクターの後に新しいオブジェクトで実行するメソッドは、クラス自体のみが知っている場合があります。これをクラス内にカプセル化するには、process.nextTickまたはPromise.resolveを使用して、さらに実行を延期し、リスナーの追加や、コンストラクターの呼び出し元であるProcess.launch内の他のものを許可します。

ほとんどすべてのコードはPromise内で実行されるため、エラーはProcess.fatalで発生します

この基本的な考え方は、特定のカプセル化のニーズに合わせて変更できます。

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