¿Cuál es la promesa explícita antipatrón de construcción y cómo la evito?

517

Estaba escribiendo código que hace algo parecido a esto:

function getStuffDone(param) {           | function getStuffDone(param) {
    var d = Q.defer(); /* or $q.defer */ |     return new Promise(function(resolve, reject) {
    // or = new $.Deferred() etc.        |     // using a promise constructor
    myPromiseFn(param+1)                 |         myPromiseFn(param+1)
    .then(function(val) { /* or .done */ |         .then(function(val) {
        d.resolve(val);                  |             resolve(val);
    }).catch(function(err) { /* .fail */ |         }).catch(function(err) {
        d.reject(err);                   |             reject(err);
    });                                  |         });
    return d.promise; /* or promise() */ |     });
}                                        | }

Alguien me dijo que esto se llama " antipatrón diferido " o " Promiseantipatrón de constructor " respectivamente, ¿qué tiene de malo este código y por qué se llama antipatrón ?

Benjamin Gruenbaum
fuente
¿Puedo confirmar que eliminar esto es (en el contexto de la derecha, no la izquierda, por ejemplo) eliminar el getStuffDonecontenedor de funciones y simplemente usar el literal Promise?
The Dembinski
1
¿o es tener el catchbloque en el getStuffDoneenvoltorio el antipatrón?
The Dembinski
1
Al menos para el Promiseejemplo nativo , también tiene envoltorios de funciones innecesarios para los manejadores .theny .catch(es decir, podría ser simplemente .then(resolve).catch(reject)). Una tormenta perfecta de antipatrones.
Noah Freitas
66
@NoahFreitas ese código está escrito de esa manera con fines didácticos. Escribí esta pregunta y respuesta para ayudar a las personas que se encuentran con este problema después de leer un montón de código que se ve así :)
Benjamin Gruenbaum
Consulte también stackoverflow.com/questions/57661537/… para saber cómo eliminar no solo la construcción explícita de Promise, sino también el uso de una variable global.
David Spector

Respuestas:

357

El antipatrón diferido (ahora antipatrón de construcción explícita) acuñado por Esailija es un antipatrón común que son nuevos en las promesas, lo hice yo mismo cuando lo usé por primera vez. El problema con el código anterior es que no puede utilizar el hecho de que promete la cadena.

Las promesas pueden encadenarse .theny usted puede devolverlas directamente. Su código getStuffDonepuede reescribirse como:

function getStuffDone(param){
    return myPromiseFn(param+1); // much nicer, right?
}

Las promesas tienen que ver con hacer que el código asincrónico sea más legible y comportarse como un código sincrónico sin ocultar ese hecho. Las promesas representan una abstracción sobre un valor de operación de una sola vez, resumen la noción de una declaración o expresión en un lenguaje de programación.

Solo debe usar objetos diferidos cuando convierte una API a promesas y no puede hacerlo automáticamente, o cuando escribe funciones de agregación que se expresan más fácilmente de esta manera.

Citando a Esailija:

Este es el antipatrón más común. Es fácil caer en esto cuando realmente no comprende las promesas y piensa en ellas como emisores de eventos glorificados o utilidad de devolución de llamada. Recapitulemos: las promesas se tratan de hacer que el código asíncrono retenga la mayoría de las propiedades perdidas del código síncrono, como la sangría plana y un canal de excepción.

Benjamin Gruenbaum
fuente
@BenjaminGruenbaum: Confío en mi uso de diferidos para esto, así que no necesito una nueva pregunta. Solo pensé que era un caso de uso que faltabas en tu respuesta. Lo que estoy haciendo parece más lo contrario de la agregación, ¿no?
mhelvens
1
@mhelvens Si está dividiendo manualmente una API sin devolución de llamada en una API de promesa que se ajusta a la parte "convertir una API de devolución de llamada en promesas". El antipatrón se trata de envolver una promesa en otra promesa sin una buena razón, no está envolviendo una promesa para empezar, por lo que no se aplica aquí.
Benjamin Gruenbaum
@BenjaminGruenbaum: Ah, aunque los diferidos se consideraban un antipatrón, ya que Bluebird los despreciaba y mencionas "convertir una API en promesas" (que también es un caso de no envolver una promesa para empezar).
mhelvens
@mhelvens Supongo que el anti patrón diferido en exceso sería más preciso para lo que realmente hace. Bluebird desaprobó la .defer()API en el constructor de promesas más nuevo (y seguro), no desaprobó (de ninguna manera) la noción de construir promesas :)
Benjamin Gruenbaum
1
Gracias @ Roamer-1888, su referencia me ayudó finalmente a descubrir cuál era mi problema. Parece que estaba creando promesas anidadas (no devueltas) sin darme cuenta.
ghuroo
135

¿Qué tiene de malo?

¡Pero el patrón funciona!

Eres afortunado. Desafortunadamente, probablemente no, ya que es probable que hayas olvidado algunos casos extremos. En más de la mitad de los casos que he visto, el autor se ha olvidado de encargarse del controlador de errores:

return new Promise(function(resolve) {
    getOtherPromise().then(function(result) {
        resolve(result.property.example);
    });
})

Si se rechaza la otra promesa, esto pasará desapercibido en lugar de propagarse a la nueva promesa (donde se manejaría), y la nueva promesa permanece pendiente para siempre, lo que puede provocar fugas.

Lo mismo sucede en el caso de que su código de devolución de llamada provoque un error, por ejemplo, cuando resultno tiene un propertyy se produce una excepción. Eso no se manejaría y dejaría la nueva promesa sin resolver.

Por el contrario, el uso se .then()ocupa automáticamente de estos dos escenarios y rechaza la nueva promesa cuando ocurre un error:

 return getOtherPromise().then(function(result) {
     return result.property.example;
 })

El antipatrón diferido no solo es engorroso, sino también propenso a errores . Usar .then()para encadenar es mucho más seguro.

¡Pero he manejado todo!

De Verdad? Bueno. Sin embargo, esto será bastante detallado y abundante, especialmente si utiliza una biblioteca prometedora que admita otras funciones como la cancelación o el envío de mensajes. ¿O tal vez lo hará en el futuro, o desea cambiar su biblioteca por una mejor? No querrás volver a escribir tu código para eso.

Los métodos de las bibliotecas ( then) no solo admiten de forma nativa todas las características, sino que también pueden tener ciertas optimizaciones. Su uso probablemente acelerará su código, o al menos permitirá que se optimice en futuras revisiones de la biblioteca.

¿Cómo lo evito?

Por lo tanto, cada vez que se encuentre creando manualmente una Promiseo una Deferredpromesa existente, consulte primero la API de la biblioteca . El antipatrón diferido a menudo es aplicado por personas que ven las promesas [solo] como un patrón de observación, pero las promesas son más que devoluciones de llamada : se supone que son componibles. Cada biblioteca decente tiene muchas funciones fáciles de usar para la composición de promesas de todas las maneras imaginables, cuidando todas las cosas de bajo nivel con las que no desea lidiar.

Si ha encontrado la necesidad de componer algunas promesas de una nueva manera que no sea compatible con una función auxiliar existente, escribir su propia función con aplazamientos inevitables debería ser su última opción. Considere cambiar a una biblioteca más funcional y / o presente un error en su biblioteca actual. Su responsable de mantenimiento debería poder derivar la composición de las funciones existentes, implementar una nueva función auxiliar para usted y / o ayudar a identificar los casos límite que deben manejarse.

Bergi
fuente
¿Hay ejemplos, además de una función que incluya setTimeout, donde el constructor podría usarse pero no considerarse "Promite constructor anitpattern"?
invitado271314
1
@ guest271314: Todo lo asincrónico que no devuelve una promesa. Aunque a menudo se obtienen mejores resultados con los ayudantes de promisificación dedicados de las bibliotecas. Y asegúrese de promisificar siempre en el nivel más bajo, de modo que no sea " una función que incluyasetTimeout ", sino " la función en setTimeoutsí misma ".
Bergi
"Y asegúrese de prometer siempre en el nivel más bajo, de modo que no sea" una función que incluya setTimeout", sino" la función setTimeoutmisma "" ¿Puede describir, vincular a las diferencias, entre los dos?
invitado271314
@ guest271314 Una función que solo incluye una llamada a setTimeoutes claramente diferente de la función en setTimeout , ¿no?
Bergi
44
Creo que una de las lecciones importantes aquí, una que no se ha establecido claramente hasta ahora, es que una Promesa y su 'entonces' encadenado representa una operación asincrónica: la operación inicial está en el constructor Promesa y el punto final eventual está en el ' entonces 'función. Entonces, si tiene una operación de sincronización seguida de una operación de asincronización, coloque las cosas de sincronización en la Promesa. Si tiene una operación asíncrona seguida de una sincronización, coloque el material de sincronización en 'entonces'. En el primer caso, devuelva la Promesa original. En el segundo caso, devuelva la cadena Promise / then (que también es Promise).
David Spector