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

时间:2014-06-25 01:10:31

标签: javascript node.js architecture constructor promise

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

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

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

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项目中。

5 个答案:

答案 0 :(得分:175)

是的,这是一种不好的做法。构造函数应该返回其类的实例,没有别的。否则会弄乱new operator和继承。

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

  

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

这应该是你班级的方法。你想改变全球状态?然后显式调用该过程,而不是生成对象的副作用。这个调用可以在实例化后立即进行:

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

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

  

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

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

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

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

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

答案 1 :(得分:10)

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

不是从构造函数返回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标志下原生使用此模式!

答案 2 :(得分: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);
        });
    }
}

答案 3 :(得分:0)

构造函数的返回值替换new运算符刚刚生成的对象,因此返回promise不是一个好主意。以前,构造函数的显式返回值用于单例模式。

ECMAScript 2017中更好的方法是使用静态方法:你有一个进程,这是静态的数字。

在构造函数之后对新对象运行哪种方法只能为类本身所知。要将它封装在类中,可以使用process.nextTick或Promise.resolve,推迟进一步执行,允许在Process.launch(构造函数的调用者)中添加侦听器和其他内容。

由于几乎所有代码都在Promise内部执行,因此错误将最终出现在Process.fatal

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

{{1}}

答案 4 :(得分:0)

这是打字稿,但应轻松转换为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;
    }

}