Veo dos patrones comunes para bloques en Objective-C. Uno es un par de éxito: / fracaso: bloques, el otro es una sola finalización: bloque.
Por ejemplo, supongamos que tengo una tarea que devolverá un objeto de forma asincrónica y esa tarea podría fallar. El primer patrón es -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure
. El segundo patrón es -taskWithCompletion:(void (^)(id object, NSError *error))completion
.
exito fracaso:
[target taskWithSuccess:^(id object) {
// W00t! I've got my object
} failure:^(NSError *error) {
// Oh noes! report the failure.
}];
terminación:
[target taskWithCompletion:^(id object, NSError *error) {
if (object) {
// W00t! I've got my object
} else {
// Oh noes! report the failure.
}
}];
¿Cuál es el patrón preferido? ¿Cuáles son las fortalezas y debilidades? ¿Cuándo usarías uno sobre el otro?
design-patterns
objective-c
Jeffery Thomas
fuente
fuente
Respuestas:
La devolución de llamada de finalización (en oposición al par éxito / fracaso) es más genérica. Si necesita preparar algún contexto antes de tratar con el estado de retorno, puede hacerlo justo antes de la cláusula "if (objeto)". En caso de éxito / fracaso, debe duplicar este código. Esto depende de la semántica de devolución de llamada, por supuesto.
fuente
-task…
pudiera devolver el objeto, pero el objeto no está en el estado correcto, aún necesitaría el manejo de errores en la condición de éxito.Diría que si la API proporciona un controlador de finalización o un par de bloques de éxito / falla, es principalmente una cuestión de preferencia personal.
Ambos enfoques tienen pros y contras, aunque solo hay diferencias marginales.
Considere que también hay otras variantes, por ejemplo cuando el uno manejador de terminación puede tener sólo uno parámetro combinando el resultado eventual o un error de potencial:
El propósito de esta firma es que un controlador de finalización se puede usar genéricamente en otras API.
Por ejemplo, en Categoría para NSArray hay un método
forEachApplyTask:completion:
que invoca secuencialmente una tarea para cada objeto y rompe el bucle IFF si hubo un error. Dado que este método también es asíncrono, también tiene un controlador de finalización:De hecho,
completion_t
como se definió anteriormente es lo suficientemente genérico y suficiente para manejar todos los escenarios.Sin embargo, hay otros medios para que una tarea asincrónica señale su notificación de finalización al sitio de la llamada:
Promesas
Las promesas, también llamadas "Futuros", "Diferidos" o "Retrasados" representan el resultado final de una tarea asincrónica (ver también: wiki Futuros y promesas ).
Inicialmente, una promesa está en el estado "pendiente". Es decir, su "valor" aún no se ha evaluado y aún no está disponible.
En Objective-C, una Promesa sería un objeto ordinario que se devolverá de un método asincrónico como se muestra a continuación:
Mientras tanto, las tareas asincrónicas comienzan a evaluar su resultado.
Tenga en cuenta también que no hay un controlador de finalización. En cambio, la Promesa proporcionará un medio más poderoso donde el sitio de la llamada puede obtener el resultado final de la tarea asincrónica, que veremos pronto.
La tarea asincrónica, que creó el objeto de promesa, DEBE eventualmente "resolver" su promesa. Eso significa que, dado que una tarea puede tener éxito o fracasar, DEBE “cumplir” una promesa que le pasa el resultado evaluado, o DEBE “rechazar” la promesa que le pasa un error que indica el motivo de la falla.
Cuando se ha resuelto una promesa, ya no puede cambiar su estado, incluido su valor.
Una vez que se ha resuelto una promesa, un sitio de llamada puede obtener el resultado (ya sea que falló o tuvo éxito). La forma en que esto se logra depende de si la promesa se implementa utilizando el estilo síncrono o asíncrono.
Una Promesa puede implementarse en un estilo síncrono o asíncrono que conduce a una semántica bloqueante o no bloqueante, respectivamente .
En un estilo sincrónico para recuperar el valor de la promesa, un sitio de llamada usaría un método que bloqueará el hilo actual hasta que la tarea asincrónica haya resuelto la promesa y el resultado final esté disponible.
En un estilo asincrónico, el sitio de la llamada registraría devoluciones de llamada o bloques de manejador que se llamarían inmediatamente después de que se haya resuelto la promesa.
Resultó que el estilo sincrónico tiene una serie de desventajas significativas que efectivamente derrotan los méritos de las tareas asincrónicas. Aquí puede leer un artículo interesante sobre la implementación actualmente defectuosa de "futuros" en la biblioteca estándar de C ++ 11: Promesas incumplidas: futuros de C ++ 0x .
¿Cómo, en Objective-C, un sitio de llamada obtendría el resultado?
Bueno, probablemente sea mejor mostrar algunos ejemplos. Hay un par de bibliotecas que implementan una Promesa (ver enlaces a continuación).
Sin embargo, para los siguientes fragmentos de código, utilizaré una implementación particular de una biblioteca Promise, disponible en GitHub RXPromise . Soy el autor de RXPromise.
Las otras implementaciones pueden tener una API similar, pero puede haber diferencias pequeñas y posiblemente sutiles en la sintaxis. RXPromise es una versión Objective-C de la especificación Promise / A + que define un estándar abierto para implementaciones robustas e interoperables de promesas en JavaScript.
Todas las bibliotecas de promesa que se enumeran a continuación implementan el estilo asincrónico.
Existen diferencias bastante significativas entre las diferentes implementaciones. RXPromise utiliza internamente despacho lib, es totalmente seguro para subprocesos, extremadamente ligero y también proporciona una serie de características útiles adicionales, como la cancelación.
Un sitio de llamada obtiene el resultado eventual de la tarea asincrónica a través de controladores de "registro". La "especificación Promise / A +" define el método
then
.El método
then
Con RXPromise se ve de la siguiente manera:
donde successHandler es un bloque que se llama cuando la promesa se ha "cumplido" y errorHandler es un bloque que se llama cuando la promesa se ha "rechazado".
En RXPromise, los bloques del controlador tienen la siguiente firma:
El success_handler tiene un resultado de parámetro que obviamente es el resultado final de la tarea asincrónica. Del mismo modo, el error_handler tiene un error de parámetro que es el error informado por la tarea asincrónica cuando falló.
Ambos bloques tienen un valor de retorno. De qué se trata este valor de retorno, pronto quedará claro.
En RXPromise,
then
es una propiedad que devuelve un bloque. Este bloque tiene dos parámetros, el bloque controlador de éxito y el bloque controlador de error. Los manejadores deben estar definidos por el sitio de la llamada.Entonces, la expresión
promise.then(success_handler, error_handler);
es una forma corta dePodemos escribir código aún más conciso:
El código dice: "Ejecute doSomethingAsync, cuando tenga éxito, luego ejecute el controlador de éxito".
Aquí, el controlador de errores es lo
nil
que significa que, en caso de error, no se manejará en esta promesa.Otro hecho importante es que llamar al bloque devuelto desde la propiedad
then
devolverá una Promesa:Al llamar al bloque devuelto desde la propiedad
then
, el "receptor" devuelve una nueva Promesa, una promesa infantil . El receptor se convierte en la promesa de los padres .Qué significa eso?
Bueno, debido a esto podemos "encadenar" tareas asincrónicas que efectivamente se ejecutan secuencialmente.
Además, el valor de retorno de cualquiera de los controladores se convertirá en el "valor" de la promesa devuelta. Entonces, si la tarea tiene éxito con el resultado final @ "OK", la promesa devuelta se "resolverá" (es decir, se "cumplirá") con el valor @ "OK":
Del mismo modo, cuando la tarea asincrónica falla, la promesa devuelta se resolverá (es decir, se "rechazará") con un error.
El controlador también puede devolver otra promesa. Por ejemplo, cuando ese controlador ejecuta otra tarea asincrónica. Con este mecanismo podemos "encadenar" tareas asincrónicas:
Si no hay promesa infantil, el valor de retorno no tiene efecto.
Un ejemplo más complejo:
Aquí, ejecutamos
asyncTaskA
,asyncTaskB
,asyncTaskC
yasyncTaskD
de forma secuencial - y cada tarea subsiguiente toma el resultado de la tarea precedente como entrada:Tal "cadena" también se llama "continuación".
Manejo de errores
Las promesas hacen que sea especialmente fácil manejar los errores. Los errores serán "reenviados" del padre al hijo si no hay un controlador de errores definido en la promesa del padre. El error se reenviará por la cadena hasta que un niño lo maneje. Por lo tanto, al tener la cadena anterior, podemos implementar el manejo de errores simplemente agregando otra "continuación" que se ocupa de un error potencial que puede ocurrir en cualquier lugar arriba :
Esto es similar al estilo síncrono probablemente más familiar con manejo de excepciones:
Las promesas en general tienen otras características útiles:
Por ejemplo, teniendo una referencia a una promesa, a través de
then
uno puede "registrar" tantos manejadores como desee. En RXPromise, los controladores de registro pueden ocurrir en cualquier momento y desde cualquier subproceso ya que es completamente seguro para subprocesos.RXPromise tiene un par de características funcionales más útiles, no requeridas por la especificación Promise / A +. Uno es "cancelación".
Resultó que la "cancelación" es una característica invaluable e importante. Por ejemplo, un sitio de llamada que contiene una referencia a una promesa puede enviarle el
cancel
mensaje para indicar que ya no está interesado en el resultado final.Imagine una tarea asincrónica que carga una imagen de la web y que se mostrará en un controlador de vista. Si el usuario se aleja del controlador de vista actual, el desarrollador puede implementar un código que envíe un mensaje de cancelación a imagePromise , que a su vez activa el controlador de errores definido por la Operación de solicitud HTTP donde se cancelará la solicitud.
En RXPromise, un mensaje de cancelación solo se reenviará de un padre a sus hijos, pero no al revés. Es decir, una promesa de "raíz" cancelará todas las promesas de los niños. Pero una promesa infantil solo cancelará la "rama" donde está el padre. El mensaje de cancelación también se enviará a los niños si ya se ha resuelto una promesa.
Una tarea asincrónica puede en sí misma registrar el controlador para su propia promesa y, por lo tanto, puede detectar cuándo alguien más la canceló. Luego, puede dejar de realizar prematuramente una tarea posiblemente larga y costosa.
Aquí hay un par de otras implementaciones de Promesas en Objective-C que se encuentran en GitHub:
https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https: //github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https: // github.com/KptainO/Rebelle
y mi propia implementación: RXPromise .
¡Es probable que esta lista no esté completa!
Al elegir una tercera biblioteca para su proyecto, compruebe cuidadosamente si la implementación de la biblioteca sigue los requisitos previos que se enumeran a continuación:
¡Una biblioteca de promesas confiable DEBE ser segura para subprocesos!
Se trata de un procesamiento asincrónico, y queremos utilizar múltiples CPU y ejecutar en diferentes hilos simultáneamente siempre que sea posible. ¡Tenga cuidado, la mayoría de las implementaciones no son seguras para subprocesos!
¡Los manejadores DEBERÁN llamarse asincrónicamente respecto del sitio de la llamada! ¡Siempre y no importa qué!
Cualquier implementación decente también debe seguir un patrón muy estricto al invocar las funciones asincrónicas. Muchos implementadores tienden a "optimizar" el caso, donde se invocará un controlador sincrónicamente cuando la promesa ya esté resuelta cuando el controlador se registre. Esto puede causar todo tipo de problemas. ¡Vea No libere a Zalgo! .
También debería haber un mecanismo para cancelar una promesa.
La posibilidad de cancelar una tarea asincrónica a menudo se convierte en un requisito con alta prioridad en el análisis de requisitos. De lo contrario, seguro que se presentará una solicitud de mejora de un usuario algún tiempo después de que se haya lanzado la aplicación. La razón debería ser obvia: cualquier tarea que pueda detenerse o demorar demasiado, debe ser cancelable por el usuario o por un tiempo de espera. Una biblioteca prometedora decente debería admitir la cancelación.
fuente
Me doy cuenta de que esta es una vieja pregunta, pero tengo que responderla porque mi respuesta es diferente a las demás.
Para aquellos que dicen que es una cuestión de preferencia personal, tengo que estar en desacuerdo. Hay una buena y lógica razón para preferir uno sobre el otro ...
En el caso de finalización, su bloque recibe dos objetos, uno representa el éxito mientras que el otro representa el fracaso ... Entonces, ¿qué hacer si ambos son nulos? ¿Qué haces si ambos tienen un valor? Estas son preguntas que pueden evitarse en el momento de la compilación y, como tales, deberían serlo. Evita estas preguntas teniendo dos bloques separados.
Tener bloques separados de éxito y fracaso hace que su código sea estáticamente verificable.
Tenga en cuenta que las cosas cambian con Swift. En él, podemos implementar la noción de una
Either
enumeración para garantizar que el bloque de finalización único tenga un objeto o un error, y debe tener exactamente uno de ellos. Entonces, para Swift, un solo bloque es mejor.fuente
Sospecho que va a terminar siendo una preferencia personal ...
Pero prefiero los bloques separados de éxito / fracaso. Me gusta separar la lógica de éxito / fracaso. Si hubiera anidado éxitos / fracasos, terminaría con algo que sería más legible (al menos en mi opinión).
Como un ejemplo relativamente extremo de tal anidamiento, aquí hay algo de Ruby que muestra este patrón.
fuente
Esto parece una copia completa, pero no creo que haya una respuesta correcta aquí. Fui con el bloque de finalización simplemente porque el manejo de errores aún debe hacerse en la condición de éxito cuando se usan bloques de éxito / falla.
Creo que el código final se verá algo así
o simplemente
No es el mejor fragmento de código y el anidamiento empeora
Creo que iré deprimido por un tiempo.
fuente