构造函数返回 Promise 是不好的做法吗?

195
我正在尝试为一个博客平台创建构造函数,里面有许多异步操作。这些操作包括从目录中获取帖子、解析帖子、通过模板引擎发送帖子等等。
我的问题是,将构造函数返回一个 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();
});

现在,用户也可以提供补充的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项目。


3
创建对象是异步操作还是获取资源是真正的异步操作?如果您使用 DI,就不会遇到这个问题。 - Benjamin Gruenbaum
15
我见过的这种问题最常见的设计模式是在构造函数中创建对象框架,然后在一个 .init() 方法中执行所有异步操作,该方法可以返回 Promise。然后将对象实例数据与异步初始化操作的构建分离。当您在对象初始化中遇到各种不同的错误(调用者想要以不同方式处理这些错误)时,同样会出现这个问题。最好从构造函数中返回对象,然后使用 .init() 返回其他内容。 - jfriend00
2
我完全同意jfriend00的观点。使用init方法来创建一个Promise是更好的实践! - Michael
1
@jfriend00 我还是不明白为什么。这种方法需要编写和维护更多的代码。 - basickarl
2
@KarlMorrison - 关于在创建新对象时执行异步操作的各种技术的讨论,请参见构造函数中的异步操作。我的个人建议是使用返回Promise的工厂函数,因为这种模式没有被误用的可能性,接口也很清晰明了。 - jfriend00
对我来说,这与滥用任何其他方法没有区别。如果一个方法返回一个Promise,而调用者没有提供.then或使用async会怎样?方法(包括构造函数)意味着一份合同。只要您的构造函数有良好的文档,警告它返回一个Promise并展示如何使用的示例,那么这就不再是您的责任了。 - cibercitizen1
5个回答

236

是的,这是一种不好的做法。构造函数应该返回它所属类的一个实例,没有其他的。否则,它会破坏new运算符和继承。

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

如果我想从我的构造函数执行某些操作怎么办?

那应该放在你的类的一个方法中。你想改变全局状态?然后显式调用这个过程,不要作为生成对象的副作用。这个调用可以放在实例化后:

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

如果这个任务是异步的,现在你可以从该方法轻松地返回一个 promise 来获取它的结果,以便轻松等待其完成。
但是,如果该方法(异步地)改变了实例并且其他方法依赖于它,则我不建议使用此模式,因为这将导致它们需要等待(即使它们实际上是同步的也会变得异步),并且您很快就会有一些内部队列管理。不要编写存在但实际上无法使用的实例。

如果我想异步地向我的实例加载数据怎么办?

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

如果答案是否定的,则在获取数据之前不应该创建它。将数据本身作为构造函数的参数,而不是告诉构造函数如何获取数据(或传递数据的 promise)。

然后,使用一个静态方法来加载数据,其中你返回一个 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);
    });
});

1
请问,“它如何“破坏了new操作符和继承”?当然,它返回一个解析为实例而不是实例的Promise。您能否解释一下这与继承有什么关系? - Shahar 'Dawn' Or
7
@mightyiam 嗯,当 (new Engine) instanceof Engine 为 false 时,这绝对是意料之外的。同样地,当你试图从那个类继承时,super(…) 会用一个 promise 来初始化 this 而不是引擎实例 - 这很混乱。不要在构造函数中使用 return - Bergi
@Bergi 这很棒!但如果在实例构造函数中声明了加载数据的路径,例如你的例子 {path: '/path/to/posts'} 中静态声明了路径,如果将路径声明在构造函数内部,那么唯一获取路径的方式就是实例化它。 - user7892649
@JordanDavis 为什么你不能将路径的声明移动到静态的 load 函数中呢?你可能想要提出一个新问题,分享你的代码并具体说明你所面临的目标和限制。 - Bergi
@Bergi 感谢您的回复,是的,我开始理解您在问题中所说的关于创建一个 static load 方法的意思了,也可以使用新的 await 如果您声明了一个 static async,那么就像 let data = await Engine.load 然后只需传递到构造函数中。 - user7892649
@Bergi,这是一个很好的解决方案,但我能否使其适用于单例模式?我的意思是,一个调用异步工厂方法并返回承诺的单例。就像这样:https://stackoverflow.com/questions/59612076/creating-an-async-singletone-in-javascript。 - omer

20

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

不要在构造函数中返回 Promise,而是将其放在 this._initialized 属性中,像这样:

function Engine(path) {
  this._initialized = Promise.resolve()
    .then(() => {
      return doSomethingAsync(path)
    })
    .then((result) => {
      this.resultOfAsyncOp = result
    })
}
  

然后,将每个方法都包裹在一个回调函数中,在初始化之后运行,就像这样:

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

从API消费者的角度来看:

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

这个可以工作的原因是您可以向承诺注册多个回调函数,它们在该承诺解决后运行,或者如果已经解决,在连接回调时运行。

这就是mongoskin的工作原理,只是它实际上并没有使用承诺。


编辑:自从我写了那个回复以后,我就爱上了ES6/7语法,所以有另一个例子使用它。

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

  async _initialize() {
    // actual async constructor logic
    this.resultOfAsyncOp = await doSomethingAsync(path)
  }

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

7
嗯,我不喜欢这种模式,因为需要“在每个方法中进行包装”。大多数情况下,这只是不必要的开销,并且当方法返回Promise时会使许多事情变得复杂,而通常它们不需要这样做。 - Bergi
3
我创建了一个npm模块,可以自动进行包装:https://www.npmjs.com/package/synchronisify - Krzysztof Kaczor
2
我知道这是一个旧的线程,但为了避免“包装每个方法”的问题,至少在Node中,使用代理很有用。 - Terrence
1
可以使用代理来完成,而不必在每个方法中调用 await this._initialized - Purefan
@Purefan 不错!你会怎么做呢? - stratis
@stratis 类似这样 https://gist.github.com/purefan/9a0d29c6123341baa37024c369a96562 在使用 promises 时效果不是很好(如果调用函数,get trap 必须返回一个函数,但是异步方法返回的是 Promise)。 - Purefan

9
为了避免关注点分离,使用工厂创建对象。
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);
        });
    }
}

8
避免使用Promise构造函数反模式 - Bergi

2
构造函数的返回值会替代 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)

0

这是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;
        // ...
        
    }
}

一般模式是使用构造函数存储承诺作为内部变量,在方法中使用await等待承诺并使方法均返回承诺。这使您可以使用async/await来避免长时间的承诺链。

我给出的示例足够处理短期承诺,但是如果输入需要长时间的承诺链,则会变得混乱,为此,请创建一个私有的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;
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接