让一个构造函数返回一个允诺是不是不好的做法?

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

所以我的问题是,让我的构造函数返回一个承诺而不是他们调用 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();
});

现在,用户还可以提供 没有补充承诺链接:

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


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

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

在构造函数中使用瓶颈的原因是有意义的。我希望整个博客在建设阶段之后都能正常运作。然而,这似乎是一种气味,几乎没有访问的对象后,立即调用 new

我已经讨论过沿着 engine.start().then()或者 engine.init()的路线使用一些东西来代替返回无极。但是那些看起来也很臭。

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

62525 次浏览

是的,这是一个糟糕的做法。构造函数应该返回其类的实例,而不是其他。否则它会搞乱 new接线员和继承。

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

如果我想从我的构造函数中执行一些东西怎么办?

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

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

如果该任务是异步的,那么您现在可以轻松地从方法返回其结果的承诺,轻松地等待它完成。
然而,当方法(异步)变异实例和其他依赖于此的方法时,我不推荐使用这种模式,因为这会导致它们被要求等待(即使它们实际上是同步的,也要变成异步的) ,并且您将很快进行一些内部队列管理。不要编写存在但实际上不可用的实例代码。

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

问问你自己: 你真的需要没有数据的实例吗? 你可以使用它吗?

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

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

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

这使得获取数据的方法具有更大的灵活性,并且极大地简化了构造函数。类似地,您可以编写返回 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);
});
});

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

将它放在 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()

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

这就是 猫皮的工作原理,只不过它实际上并不使用承诺。


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

}

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

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

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

ECMAScript 2017中更好的方法是使用静态方法: 您有一个进程,它是 static 的数量。

在构造函数之后在新对象上运行的方法可能只有类本身知道。为了在类中封装它,您可以使用 process.nextTick 或者 Promise.decision,延迟进一步的执行,以便在构造函数的调用程序 Process.unch 中添加侦听器和其他内容。

因为几乎所有的代码都是在一个  承諾中执行的,所以所有的错误最後都會在 Process.death 中發生

可以修改这个基本思想以适应特定的封装需求。

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)

这是在类型,但应该很容易转换为 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> {
// ...
}


}

这里是一个更加充实的爱奥尼亚/角的例子

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


}