【问题标题】:Is it bad practice to have a constructor function return a Promise?让构造函数返回 Promise 是不好的做法吗?
【发布时间】:2014-08-15 10:16:13
【问题描述】:

我正在尝试为博客平台创建一个构造函数,它内部正在进行许多异步操作。这些范围包括从目录中抓取帖子、解析它们、通过模板引擎发送它们等等。

所以我的问题是,让我的构造函数返回一个 promise 而不是他们调用 new 反对的函数的对象是不明智的。

例如:

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

现在,用户也可以提供补充的 Promise 链环:

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

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

这可能会造成问题,因为用户可能会感到困惑,为什么 engine 在构建后不可用。

在构造函数中使用 Promise 的原因是有道理的。我希望整个博客在构建阶段之后能够正常运行。但是,在调用new 后几乎无法立即访问该对象似乎是一种气味。

我已经讨论过使用类似于engine.start().then()engine.init() 的东西来代替返回 Promise。但那些看起来也很臭。

编辑:这是在 Node.js 项目中。

【问题讨论】:

  • 创建对象是异步操作还是获取资源真的是异步操作?如果你使用 DI,我认为你不会有这个问题
  • 对于此类问题,我见过的最常见的设计模式是在构造函数中创建对象外壳,然后在 .init() 方法中执行所有异步操作,然后可以返回承诺。然后从异步初始化操作中分离出对象中的实例数据和该对象的构造。当您在对象的初始化过程中出现各种不同的错误(调用者希望以不同的方式处理)时,也会出现同样的问题。从构造函数返回对象然后使用.init()返回其他东西会更好。
  • 我完全同意 jfriend00。最好使用 init 方法来做出承诺!
  • @jfriend00 我还是不明白为什么。用这种方法编写和维护更多代码。
  • @KarlMorrison - 有关在创建新对象时执行异步操作的各种技术的讨论,请参阅Asynchronous Operations in Constructor。我个人的推荐是一个返回 promise 的工厂函数,因为没有办法意外误用该模式,而且界面清晰明了。

标签: javascript node.js architecture constructor promise


【解决方案1】:

这是在 typescript 中,但应该很容易转换为 ECMAscript

export class Cache {
    private aPromise: Promise<X>;
    private bPromise: Promise<Y>;
    constructor() {
        this.aPromise = new Promise(...);
        this.bPromise = new Promise(...);
    }
    public async saveFile: Promise<DirectoryEntry> {
        const aObject = await this.aPromise;
        // ...
        
    }
}

一般模式是使用构造函数将 Promise 存储为内部变量,await 用于方法中的 Promise,并使方法都返回 Promise。这允许您使用async/await 来避免长承诺链。

我给出的示例对于简短的 Promise 来说已经足够了,但是放入一些需要长 Promise 链的东西会使这变得混乱,所以为了避免这种情况,请创建一个将由构造函数调用的私有 async 方法。

export class Cache {
    private aPromise: Promise<X>;
    private bPromise: Promise<Y>;
    constructor() {
        this.aPromise = initAsync();
        this.bPromise = new Promise(...);
    }
    public async saveFile: Promise<DirectoryEntry> {
        const aObject = await this.aPromise;
        // ...
        
    }
    private async initAsync() : Promise<X> {
        // ...
    }

}

这是 Ionic/Angular 的一个更充实的示例

import { Injectable } from "@angular/core";
import { DirectoryEntry, File } from "@ionic-native/file/ngx";

@Injectable({
    providedIn: "root"
})
export class Cache {
    private imageCacheDirectoryPromise: Promise<DirectoryEntry>;
    private pdfCacheDirectoryPromise: Promise<DirectoryEntry>;

    constructor(
        private file: File
    ) {
        this.imageCacheDirectoryPromise = this.initDirectoryEntry("image-cache");
        this.pdfCacheDirectoryPromise = this.initDirectoryEntry("pdf-cache");
    }

    private async initDirectoryEntry(cacheDirectoryName: string): Promise<DirectoryEntry> {
        const cacheDirectoryEntry = await this.resolveLocalFileSystemDirectory(this.file.cacheDirectory);
        return this.file.getDirectory(cacheDirectoryEntry as DirectoryEntry, cacheDirectoryName, { create: true })
    }

    private async resolveLocalFileSystemDirectory(path: string): Promise<DirectoryEntry> {
        const entry = await this.file.resolveLocalFilesystemUrl(path);
        if (!entry.isDirectory) {
            throw new Error(`${path} is not a directory`)
        } else {
            return entry as DirectoryEntry;
        }
    }

    public async imageCacheDirectory() {
        return this.imageCacheDirectoryPromise;
    }

    public async pdfCacheDirectory() {
        return this.pdfCacheDirectoryPromise;
    }

}

【讨论】:

    【解决方案2】:

    我遇到了同样的问题,想出了这个简单的解决方案。

    不要从构造函数返回 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()
    

    这是可行的,因为您可以向一个 Promise 注册多个回调,它们可以在它解析后运行,或者如果它已经解析,则在附加回调时运行。

    mongoskin 就是这样工作的,只不过它实际上并没有使用 Promise。


    编辑:自从我写了那个回复后,我就爱上了 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
      }
    
    }
    

    编辑:您可以将此模式与节点 7 和--harmony 标志一起使用!

    【讨论】:

    • 嗯,我不喜欢这种模式,因为需要“包装每个方法”。大多数时候这只是不必要的开销,并且当方法确实返回承诺而通常不需要这样做时,会使许多事情复杂化。
    • 我创建了自动包装的 npm 模块:npmjs.com/package/synchronisify
    • 我知道这是一个旧线程,但为了避免“包装每个方法”的问题,至少在 Node 中,代理很有用。
    【解决方案3】:

    构造函数的返回值替换了 new 运算符刚刚生成的对象,因此返回一个 Promise 不是一个好主意。以前,构造函数的显式返回值用于单例模式。

    ECMAScript 2017 中更好的方法是使用静态方法:你有一个过程,即静态的数字化。

    在构造函数之后在新对象上运行哪个方法可能只有类本身知道。要将其封装在类中,您可以使用 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)
    

    【讨论】:

      【解决方案4】:

      为避免关注点分离,请使用工厂创建对象。

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

      【讨论】:

      【解决方案5】:

      是的,这是一种不好的做法。构造函数应该返回其类的实例,仅此而已。否则会弄乱new operator 和继承。

      此外,构造函数应该只创建和初始化一个新实例。它应该设置数据结构和所有特定于实例的属性,但不执行任何任务。如果可能,它应该是一个没有副作用的pure function,并具有所有的好处。

      如果我想从我的构造函数中执行事情怎么办?

      这应该在你的类的方法中。你想改变全局状态吗?然后显式调用该过程,而不是作为生成对象的副作用。此调用可以在实例化之后立即进行:

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

      如果该任务是异步的,您现在可以轻松地从该方法返回其结果的 Promise,以便轻松地等待它完成。
      但是,当方法(异步)改变实例并且其他方法依赖于它时,我不推荐这种模式,因为这会导致它们需要等待(即使它们实际上是同步的,也会变成异步的)并且你很快就会有一些内部队列管理正在进行中。不要将实例编码为存在但实际上不可用。

      如果我想将数据异步加载到我的实例中怎么办?

      问问自己:你真的需要没有数据的实例吗?你能以某种方式使用它吗?

      如果答案是,那么您不应该在拥有数据之前创建它。将数据 ifself 作为构造函数的参数,而不是告诉构造函数如何获取数据(或传递数据的承诺)。

      然后,使用静态方法加载数据,并从中返回一个 Promise。然后链接一个将数据包装在一个新实例中的调用:

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

      【讨论】:

      • 请问它究竟是如何“搞乱新的运算符和继承”的?当然,它返回一个解析为实例而不是实例的承诺。你能解释一下这与继承有什么关系吗?
      • @mightyiam 好吧,当(new Engine) instanceof Engine 为假时,这绝对是出乎意料的。同样,当您尝试从该类继承时,super(…) 初始化 this 并承诺不是引擎实例——一团糟。不要来自构造函数的return
      • @Bergi 这太棒了!但是如果加载数据的路径是在实例构造函数中声明的,在你的例子中{path: '/path/to/posts'}这是静态声明的,如果你在构造函数中声明路径,那么获取它的唯一方法就是实例化它。
      • @JordanDavis 为什么不能将路径的声明移动到静态load 函数中?您可能希望ask a new question 分享您的代码并具体说明您的目标和面临的限制。
      • @Bergi 感谢您的回复,是的,我开始明白您在关于创建static load 方法的问题中所说的内容,如果您声明@987654337,也可以使用新的await @ 就像let data = await Engine.load 然后传递给构造函数。
      猜你喜欢
      • 1970-01-01
      • 2013-03-31
      • 2015-07-10
      • 1970-01-01
      • 2010-11-23
      相关资源
      最近更新 更多