javascript - sincronas - ¿Es una mala práctica tener una función de constructor para devolver una Promesa?
promise typescript (4)
Intento crear un constructor para una plataforma de blogging y tiene muchas operaciones asincrónicas dentro. Estos van desde agarrar las publicaciones de directorios, analizarlas, enviarlas a través de motores de plantillas, etc.
Entonces mi pregunta es, ¿no sería prudente que mi función constructora devuelva una promesa en lugar de un objeto de la función contra la que llamaron new
?
Por ejemplo:
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();
});
Ahora, el usuario tampoco puede suministrar un suplemento de enlace de cadena Promesa:
var engine = new Engine({path: ''/path/to/posts''});
// ERROR
// engine will not be available as an Engine object here
Esto podría plantear un problema ya que el usuario puede confundirse por qué el engine
no está disponible después de la construcción.
La razón para usar una Promesa en el constructor tiene sentido. Quiero que todo el blog esté funcionando después de la fase de construcción. Sin embargo, parece como un olor casi no tener acceso al objeto inmediatamente después de llamar a new
.
He debatido el uso de algo parecido a engine.start().then()
engine.init()
o engine.init()
que devolvería la Promesa en su lugar. Pero esos también parecen malolientes.
Editar: Esto está en un proyecto Node.js.
El valor de retorno del constructor reemplaza el objeto que el nuevo operador acaba de producir, por lo que devolver una promesa no es una buena idea. Anteriormente, se utilizaba un valor de retorno explícito del constructor para el patrón singleton.
La mejor manera en ECMAScript 2017 es usar métodos estáticos: tiene un proceso, que es la numeración de estática.
Qué método ejecutar en el nuevo objeto después del constructor puede ser conocido solo por la clase. Para encapsular esto dentro de la clase, puede usar process.nextTick o Promise.resolve, posponiendo la ejecución adicional permitiendo que los oyentes se agreguen y otras cosas en Process.launch, el invocador del constructor.
Como casi todo el código se ejecuta dentro de una Promesa, los errores terminarán en Process.fatal
Esta idea básica se puede modificar para adaptarse a las necesidades específicas de encapsulación.
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)
Me encontré con el mismo problema y se me ocurrió esta sencilla solución.
En lugar de devolver una Promesa del constructor, this.initialization
en this.initialization
propiedad de this.initialization
, como esta:
function Engine(path) {
var engine = this
engine.initialization = Promise.resolve()
.then(function () {
return doSomethingAsync(path)
})
.then(function (result) {
engine.resultOfAsyncOp = result
})
}
Luego, ajuste cada método en una devolución de llamada que se ejecute después de la inicialización, así:
Engine.prototype.showPostsOnPage = function () {
return this.initialization.then(function () {
// actual body of the method
})
}
Cómo se ve desde la perspectiva del consumidor API:
engine = new Engine({path: ''/path/to/posts''})
engine.showPostsOnPage()
Esto funciona porque puede registrar múltiples devoluciones de llamada a una promesa y se ejecutan después de que se resuelve o, si ya está resuelto, en el momento de adjuntar la devolución de llamada.
Así es como funciona mongoskin , excepto que en realidad no usa promesas.
Editar: Desde que escribí esa respuesta, me enamoré de la sintaxis de ES6 / 7, así que hay otro ejemplo que usa eso. Puedes usarlo hoy con 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
}
}
Editar : ¡Puedes usar este patrón de forma nativa con el nodo 7 y la bandera de --harmony
!
Para evitar la separación de preocupaciones, use una fábrica para crear el objeto.
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);
});
}
}
Sí, es una mala práctica. Un constructor debe devolver una instancia de su clase, nada más. Sería un desastre para el new
operador y la herencia de lo contrario.
Además, un constructor solo debería crear e inicializar una nueva instancia. Debe configurar estructuras de datos y todas las propiedades específicas de la instancia, pero no ejecutar ninguna tarea. Debe ser una función pura sin efectos secundarios si es posible, con todos los beneficios que tiene.
¿Qué sucede si quiero ejecutar cosas desde mi constructor?
Eso debería ir en un método de tu clase. ¿Quieres mutar el estado global? Luego llame a ese procedimiento explícitamente, no como un efecto secundario de generar un objeto. Esta llamada puede ir inmediatamente después de la instanciación:
var engine = new Engine()
engine.displayPosts();
Si esa tarea es asíncrona, ahora puede devolver fácilmente una promesa de los resultados del método, para esperar fácilmente hasta que finalice.
Sin embargo, no recomendaría este patrón cuando el método (asincrónicamente) muta la instancia y otros métodos dependen de eso, ya que eso los llevaría a esperar (se vuelvan asincrónicos incluso si son realmente sincrónicos) y usted tendría rápidamente un poco de administración de colas interna pasando. No codifique las instancias para que existan pero que realmente no se puedan usar.
¿Qué ocurre si quiero cargar datos en mi instancia de forma asíncrona?
Pregúntese: ¿realmente necesita la instancia sin los datos? ¿Podrías usarlo de alguna manera?
Si la respuesta es No , entonces no debería crearlo antes de tener los datos. Haga los datos solo un parámetro para su constructor, en lugar de decirle al constructor cómo recuperar los datos (o pasar una promesa para los datos).
Luego, use un método estático para cargar los datos, a partir de los cuales devuelve una promesa. Luego encadena una llamada que envuelve los datos en una nueva instancia sobre eso:
Engine.load({path: ''/path/to/posts''}).then(function(posts) {
new Engine(posts).displayPosts();
});
Esto permite una flexibilidad mucho mayor en las formas de adquirir los datos y simplifica mucho al constructor. Del mismo modo, puede escribir funciones de fábrica estáticas que devuelvan promesas para las instancias del 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);
});
});