Неужели плохая практика заключается в том, что функция-конструктор возвращает обещание?

Я пытаюсь создать конструктор для платформы ведения блогов, и в нем много асинхронных операций. Они варьируются от захвата сообщений из каталогов, их разбора, отправки их через движки шаблонов и т.д.

Итак, мой вопрос: было бы неразумно, чтобы моя функция-конструктор возвращала обещание вместо объекта функции, которую они называли 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(), которое вместо этого вернет Promise. Но они также кажутся вонючей.

Изменить: это в проекте Node.js.

Ответ 1

Да, это плохая практика. Конструктор должен возвращать экземпляр своего класса, ничего другого. Это испортит new оператор и наследование в противном случае.

Кроме того, конструктор должен создавать и инициализировать новый экземпляр. Он должен настраивать структуры данных и все специфичные для экземпляра свойства, но не выполнять любые задачи. Это должна быть чистая функция без побочных эффектов, если это возможно, со всеми преимуществами.

Что делать, если я хочу выполнять вещи из моего конструктора?

Это должно идти в методе вашего класса. Вы хотите изменить глобальное состояние? Затем вызовите эту процедуру явно, а не как побочный эффект создания объекта. Этот вызов может идти сразу после создания:

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

Если эта задача является асинхронной, теперь вы можете легко вернуть обещание для своих результатов из метода, чтобы легко подождать, пока она не будет завершена.
Однако я бы не рекомендовал этот шаблон, когда метод (асинхронно) мутирует экземпляр и другие методы, зависит от этого, поскольку это приведет к тому, что им потребуется ждать (стать асинхронным, даже если они фактически синхронны), и вы быстро некоторое внутреннее управление очередями продолжается. Не создавайте экземпляры экземпляров, но на самом деле непригодны для использования.

Что делать, если я хочу асинхронно загружать данные в мой экземпляр?

Спросите себя: вам действительно нужен экземпляр без данных? Не могли бы вы его использовать?

Если ответ на это Нет, тогда вы не должны создавать его, прежде чем у вас есть данные. Сделайте данные ifself параметром для своего конструктора, вместо того, чтобы сообщать конструктору, как извлекать данные (или передавать обещание для данных).

Затем используйте статический метод для загрузки данных, с которых вы возвращаете обещание. Затем соедините вызов, который обертывает данные в новом экземпляре:

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

Это позволяет значительно повысить гибкость в способах получения данных и значительно упростить конструктор. Аналогично, вы можете написать статические функции factory, которые возвращают promises для экземпляров 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);
    });
});

Ответ 2

Я столкнулся с той же проблемой и придумал это простое решение.

Вместо того, чтобы возвращать 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, за исключением того, что он фактически не использует promises.


Изменить:. Так как я написал этот ответ, я влюбился в синтаксис 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
  }

}

Изменить. Вы можете использовать этот шаблон изначально с флагом node 7 и --harmony!

Ответ 3

Чтобы избежать разделения проблем, используйте factory для создания объекта.

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

Ответ 4

Возвращаемое значение конструктора заменяет объект, созданный новым оператором, поэтому возвращение обещания не является хорошей идеей. Ранее для шаблона singleton использовалось явное возвращаемое значение конструктора.

Лучший способ в ECMAScript 2017 - использовать статические методы: у вас есть один процесс, который является числом статических.

Какой метод запускать новый объект после конструктора может быть известен только самому классу. Чтобы инкапсулировать это внутри класса, вы можете использовать process.nextTick или Promise.resolve, откладывая дальнейшее выполнение, позволяющее добавлять слушателей и другие вещи в Process.launch, invoker конструктора.

Так как почти весь код выполняется внутри 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)