'Is it bad practice to have a constructor function return a Promise?
I'm trying to create a constructor for a blogging platform and it has many async operations going on inside. These range from grabbing the posts from directories, parsing them, sending them through template engines, etc.
So my question is, would it be unwise to have my constructor function return a promise instead of an object of the function they called new against.
For instance:
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();
});
Now, the user may also not supply a supplement Promise chain link:
var engine = new Engine({path: '/path/to/posts'});
// ERROR
// engine will not be available as an Engine object here
This could pose a problem as the user may be confused why engine is not available after construction.
The reason to use a Promise in the constructor makes sense. I want the entire blog to be functioning after the construction phase. However, it seems like a smell almost to not have access to the object immediately after calling new.
I have debated using something along the lines of engine.start().then() or engine.init() which would return the Promise instead. But those also seem smelly.
Edit: This is in a Node.js project.
Solution 1:[1]
I encountered the same problem and came up with this simple solution.
Instead of returning a Promise from the constructor, put it in this.initialization property, like this:
function Engine(path) {
var engine = this
engine.initialization = Promise.resolve()
.then(function () {
return doSomethingAsync(path)
})
.then(function (result) {
engine.resultOfAsyncOp = result
})
}
Then, wrap every method in a callback that runs after the initialization, like that:
Engine.prototype.showPostsOnPage = function () {
return this.initialization.then(function () {
// actual body of the method
})
}
How it looks from the API consumer perspective:
engine = new Engine({path: '/path/to/posts'})
engine.showPostsOnPage()
This works because you can register multiple callbacks to a promise and they run either after it resolves or, if it's already resolved, at the time of attaching the callback.
This is how mongoskin works, except it doesn't actually use promises.
Edit: Since I wrote that reply I've fallen in love with ES6/7 syntax so there's another example using that.
class Engine {
constructor(path) {
this._initialized = this._initialize()
}
async _initialize() {
// actual async constructor logic
}
async showPostsOnPage() {
await this._initialized
// actual body of the method
}
}
Solution 2:[2]
To avoid the separation of concerns, use a factory to create the object.
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);
});
}
}
Solution 3:[3]
The return value from the constructor replaces the object that the new operator just produced, so returning a promise is not a good idea. Previously, an explicit return value from the constructor was used for the singleton pattern.
The better way in ECMAScript 2017 is to use a static methods: you have one process, which is the numerality of static.
Which method to be run on the new object after the constructor may be known only to the class itself. To encapsulate this inside the class, you can use process.nextTick or Promise.resolve, postponing further execution allowing for listeners to be added and other things in Process.launch, the invoker of the constructor.
Since almost all code executes inside of a Promise, errors will end up in Process.fatal
This basic idea can be modified to fit specific encapsulation needs.
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)
Solution 4:[4]
This is in typescript, but should be easily converted to 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;
// ...
}
}
The general pattern is to store the promises as internal variables using the constructor and await for the promises in the methods and make the methods all return promises. This allows you to use async/await to avoid long promise chains.
The example I gave is good enough for short promises, but putting in something that requires a long promise chain will make this messy, so to avoid that create a private async method that will be called by the constructor.
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> {
// ...
}
}
Here is a more fleshed out example for 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;
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | sth |
| Solution 3 | |
| Solution 4 |
