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

Is it bad practice to have a constructor function return a Promise?

提问人:adam-beck 提问时间:6/25/2014 最后编辑:Adam Beckadam-beck 更新时间:10/19/2022 访问量:67398

问:

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

所以我的问题是,让我的构造函数返回一个 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()

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

JavaScript 节点.js 架构 构造函数 承诺

评论

3赞 Benjamin Gruenbaum 6/25/2014
创建对象是异步操作,还是获取其资源真的是异步操作?我想如果你使用 DI,你就不会有这个问题
15赞 jfriend00 6/25/2014
对于此类问题,我见过的最常见的设计模式是只需在构造函数中创建对象 shell,然后在可以返回 promise 的方法中执行所有异步操作。然后,将对象中的实例数据以及该对象的构造与异步初始化操作分离出来。当您在对象初始化过程中可能发生各种不同的错误(调用方希望以不同的方式处理这些错误)时,会出现同样的问题。最好从构造函数返回对象,然后用于返回其他内容。.init().init()
2赞 Michael 6/26/2014
我完全同意 jfriend00。最好使用 init 方法来做出承诺!
1赞 basickarl 11/29/2019
@jfriend00我仍然不明白为什么。在此方法中要编写和维护更多代码。
2赞 jfriend00 11/30/2019
@KarlMorrison - 有关在创建新对象时执行异步操作的各种技术的讨论,请参阅构造函数中的异步操作。我个人的建议是一个返回 promise 的工厂函数,因为没有办法意外地滥用该模式,并且接口清晰明了。

答:

236赞 Bergi 7/11/2014 #1

是的,这是一种不好的做法。构造函数应该返回其类的实例,而不是其他任何实例。否则,它会弄乱新的运算符和继承。

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

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

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

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

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

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

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

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

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

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

这为获取数据的方式提供了更大的灵活性,并大大简化了构造函数。同样,您可以编写静态工厂函数来返回实例的 promise:Engine

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赞 Shahar 'Dawn' Or 1/13/2017
请问它究竟是如何“搞砸新的运算符和继承”的?当然,它返回一个解析为实例而不是实例的 promise。您能解释一下这与继承有什么关系吗?
7赞 Bergi 1/13/2017
@mightyiam 好吧,什么时候是假的,这绝对是出乎意料的。同样,当您尝试从该类继承时,初始化时会使用承诺而不是引擎实例 - 一团糟。不要来自构造函数。(new Engine) instanceof Enginesuper(…)thisreturn
0赞 9/24/2017
@Bergi这太棒了!但是,如果加载数据的路径是在实例构造函数中声明的,在您的示例中,这是静态声明的,如果您在构造函数中声明路径,那么获取它的唯一方法是实例化它,该怎么办?{path: '/path/to/posts'}
0赞 Bergi 9/24/2017
@JordanDavis 为什么不能将路径的声明移动到静态函数中?你可能想问一个新问题来分享你的代码,并具体说明你的目标和你面临的限制。load
0赞 9/25/2017
@Bergi感谢您的回复,是的,我开始明白您在问题中所说的关于创建方法的内容,如果您声明一个如此相似的方法,也可以使用新的方法,然后直接传递到构造函数中。static loadawaitstatic asynclet data = await Engine.load
20赞 phaux 7/3/2015 #2

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

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

这之所以有效,是因为您可以向一个 promise 注册多个回调,它们要么在解析后运行,要么在附加回调时运行(如果已经解析)。

这就是mongoskin的工作方式,只是它实际上并不使用promises。


编辑:自从我写了那个回复后,我就爱上了 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赞 Bergi 7/5/2015
嗯,我不喜欢这种模式,因为需要“包装每个方法”。大多数时候,这只是不必要的开销,当方法返回承诺时,它们通常不需要返回,这会使许多事情复杂化。
3赞 Krzysztof Kaczor 7/10/2016
我创建了npm模块,它使包装自动:npmjs.com/package/synchronisify
2赞 Terrence 8/3/2018
我知道这是一个旧线程,但为了避免“包装每个方法”问题,至少在 Node 中,代理很有用。
1赞 Purefan 5/5/2022
也可以使用代理来完成,而不必调用每个方法await this._initialized
0赞 stratis 9/21/2022
@Purefan 不错!你会怎么做?
9赞 The Farmer 11/23/2016 #3

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

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赞 Bergi 1/13/2017
避免 Promise 构造函数反模式
2赞 Harald Rudell 1/29/2017 #4

构造函数的返回值替换了 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赞 Archimedes Trajano 11/4/2020 #5

这是打字稿,但应该很容易转换为 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 存储为内部变量,并在方法中将 promise 存储为内部变量,并使所有方法都返回 promise。这允许您使用 / 来避免长 promise 链。awaitasyncawait

我给出的示例对于短承诺来说已经足够好了,但是放入需要长承诺链的东西会使它变得混乱,因此为了避免这种情况,请创建一个由构造函数调用的私有方法。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;
    }

}