Это плохая практика, чтобы функция конструктора возвращала 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.
4 ответа
Да, это плохая практика. Конструктор должен возвращать экземпляр своего класса, ничего больше. Было бы испортить new
оператор и наследование в противном случае.
Более того, конструктор должен только создавать и инициализировать новый экземпляр. Он должен устанавливать структуры данных и все специфичные для экземпляра свойства, но не выполнять никаких задач. Это должна быть чистая функция без побочных эффектов, если это возможно, со всеми преимуществами, которые имеет.
Что если я захочу выполнить вещи из моего конструктора?
Это должно идти в методе вашего класса. Вы хотите изменить мировое состояние? Затем вызовите эту процедуру явно, а не как побочный эффект генерации объекта. Этот вызов может идти сразу после создания экземпляра:
var engine = new Engine()
engine.displayPosts();
Если эта задача асинхронная, теперь вы можете легко вернуть обещание ее результатов из метода, чтобы легко дождаться ее завершения.
Однако я бы не рекомендовал этот шаблон, когда метод (асинхронно) мутирует экземпляр, и другие методы зависят от этого, так как это приведет к тому, что они будут вынуждены ждать (стать асинхронными, даже если они на самом деле синхронны), и вы быстро получите происходит некоторое внутреннее управление очередью. Не кодируйте экземпляры для существования, но на самом деле их нельзя использовать.
Что если я хочу загрузить данные в мой экземпляр асинхронно?
Задайте себе вопрос: нужен ли вам экземпляр без данных? Не могли бы вы использовать это как-нибудь?
Если ответ " Нет", то вам не следует создавать его до того, как вы получите данные. Сделайте данные ifself параметром для вашего конструктора, вместо того, чтобы указывать конструктору, как извлечь данные (или передать обещание для данных).
Затем используйте статический метод для загрузки данных, из которых вы возвращаете обещание. Затем соедините вызов, который упаковывает данные в новый экземпляр этого:
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);
});
});
Я столкнулся с той же проблемой и придумал это простое решение.
Вместо того, чтобы возвращать 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()
Это работает, потому что вы можете зарегистрировать несколько обратных вызовов к обещанию, и они запускаются либо после его разрешения, либо, если оно уже разрешено, во время присоединения обратного вызова.
Так работает mongoskin, за исключением того, что он фактически не использует обещания.
Изменить: так как я написал этот ответ, я влюбился в синтаксис 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
флаг!
Чтобы избежать разделения интересов, используйте фабрику для создания объекта.
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);
});
}
}
Возвращаемое значение из конструктора заменяет объект, который только что создал новый оператор, поэтому возвращение обещания не является хорошей идеей. Ранее для одноэлементного шаблона использовалось явное возвращаемое значение из конструктора.
Лучший способ в 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)
Это машинописный текст, но его легко преобразовать в 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;
}
}