éxito: / fracaso: bloques vs finalización: bloque

23

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?

Jeffery Thomas
fuente
Estoy bastante seguro de que Objective-C tiene un manejo de excepciones con throw / catch, ¿hay alguna razón por la que no pueda usar eso?
FrustratedWithFormsDesigner
Cualquiera de estos permite encadenar llamadas asíncronas, que excepciones no le dan.
Frank Shearar el
55
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - idiomatic objc no usa try / catch para el control de flujo.
Ant
1
Considere trasladar su respuesta de la pregunta a una respuesta ... después de todo, es una respuesta (y puede responder sus propias preguntas).
1
Finalmente cedí a la presión de grupo y moví mi respuesta a una respuesta real.
Jeffery Thomas

Respuestas:

8

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
No puedo comentar sobre la pregunta original ... Las excepciones no son un control de flujo válido en el objetivo-c (bueno, cacao) y no deben usarse como tales. La excepción lanzada debe capturarse solo para terminar con gracia.
Sí, puedo ver eso. Si -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.
Jeffery Thomas el
Sí, y si el bloque no está en su lugar, pero se pasa como argumento a su controlador, debe lanzar dos bloques. Esto puede ser aburrido cuando la devolución de llamada necesita pasar a través de muchas capas. Sin embargo, siempre puedes dividirlo / componerlo.
No entiendo cómo el controlador de finalización es más genérico. La finalización básicamente convierte los parámetros de varios métodos en uno, en forma de parámetros de bloque. Además, ¿genérico significa mejor? En MVC, a menudo también tiene un código duplicado en el controlador de vista, que es un mal necesario debido a la separación de las preocupaciones. Sin embargo, no creo que sea una razón para mantenerse alejado de MVC.
Boon
@Boon Una razón por la que veo que el controlador único es más genérico es para los casos en los que preferiría que la persona que llama / manejador / bloque en sí mismo determine si una operación tuvo éxito o no. Considere casos de éxito parcial en los que posiblemente tenga un objeto con datos parciales y su objeto de error sea un error que indica que no se devolvieron todos los datos. El bloque podría examinar los datos en sí y verificar si son suficientes. Esto no es posible con el escenario de devolución de llamada de éxito / error binario.
Travis
8

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:

typedef void (^completion_t)(id result);

- (void) taskWithCompletion:(completion_t)completionHandler;

[self taskWithCompletion:^(id result){
    if ([result isKindOfError:[NSError class]) {
        NSLog(@"Error: %@", result);
    }
    else {
        ...
    }
}]; 

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:

typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);

De hecho, completion_tcomo 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:

- (Promise*) doSomethingAsync;

! El estado inicial de una promesa es "pendiente".

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.

! Una tarea finalmente debe resolver su promesa.

Cuando se ha resuelto una promesa, ya no puede cambiar su estado, incluido su valor.

! Una promesa solo puede resolverse una vez .

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:

promise.then(successHandler, errorHandler);

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".

! thense utiliza para obtener el resultado final y para definir un controlador de éxito o error.

En RXPromise, los bloques del controlador tienen la siguiente firma:

typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);

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, thenes 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.

! 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 de

then_block_t block promise.then;
block(success_handler, error_handler);

Podemos escribir código aún más conciso:

doSomethingAsync
.then(^id(id result){
    
    return @“OK”;
}, nil);

El código dice: "Ejecute doSomethingAsync, cuando tenga éxito, luego ejecute el controlador de éxito".

Aquí, el controlador de errores es lo nilque significa que, en caso de error, no se manejará en esta promesa.

Otro hecho importante es que llamar al bloque devuelto desde la propiedad thendevolverá una Promesa:

! then(...)devuelve 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 .

RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);

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":

RXPromise* returnedPromise = asyncA().then(^id(id result){
    return @"OK";
}, nil);

...
assert([[returnedPromise get] isEqualToString:@"OK"]);

Del mismo modo, cuando la tarea asincrónica falla, la promesa devuelta se resolverá (es decir, se "rechazará") con un error.

RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
    return error;
});

...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);

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:

RXPromise* returnedPromise = asyncA().then(^id(id result){
    return asyncB(result);
}, nil);

! El valor de retorno de un bloque controlador se convierte en el valor de la promesa secundaria.

Si no hay promesa infantil, el valor de retorno no tiene efecto.

Un ejemplo más complejo:

Aquí, ejecutamos asyncTaskA, asyncTaskB, asyncTaskCy asyncTaskD de forma secuencial - y cada tarea subsiguiente toma el resultado de la tarea precedente como entrada:

asyncTaskA()
.then(^id(id result){
    return asyncTaskB(result);
}, nil)
.then(^id(id result){
    return asyncTaskC(result);
}, nil)
.then(^id(id result){
    return asyncTaskD(result);
}, nil)
.then(^id(id result){
    // handle result
    return nil;
}, nil);

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 :

asyncTaskA()
.then(^id(id result){
    return asyncTaskB(result);
}, nil)
.then(^id(id result){
    return asyncTaskC(result);
}, nil)
.then(^id(id result){
    return asyncTaskD(result);
}, nil)
.then(^id(id result){
    // handle result
    return nil;
}, nil);
.then(nil, ^id(NSError*error) {
    NSLog(@“”Error: %@“, error);
    return nil;
});

Esto es similar al estilo síncrono probablemente más familiar con manejo de excepciones:

try {
    id a = A();
    id b = B(a);
    id c = C(b);
    id d = D(c);
    // handle d
}
catch (NSError* error) {
    NSLog(@“”Error: %@“, error);
}

Las promesas en general tienen otras características útiles:

Por ejemplo, teniendo una referencia a una promesa, a través de thenuno 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 cancelmensaje 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.

CouchDeveloper
fuente
1
Esto obtiene el premio por la falta de respuesta más larga. Pero A por esfuerzo :-)
Travelling Man
3

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 Eitherenumeració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.

Daniel T.
fuente
1

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.

Frank Shearar
fuente
1
He visto cadenas anidadas de ambos. Creo que ambos se ven terribles, pero esa es mi opinión personal.
Jeffery Thomas el
1
Pero, ¿de qué otra manera podría encadenar llamadas asíncronas?
Frank Shearar el
No lo sé hombre ... No lo sé. Parte de la razón por la que pregunto es porque no me gusta cómo se ve mi código asincrónico.
Jeffery Thomas el
Seguro. Terminas escribiendo tu código en un estilo de paso continuo, lo cual no es terriblemente sorprendente. (Haskell tiene su notación de hacer exactamente por esta razón: permitiéndole escribir en un estilo aparentemente directo.)
Frank Shearar el
Te puede interesar esta implementación de ObjC Promises: github.com/couchdeveloper/RXPromise
e1985
0

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í

[target taskWithCompletion:^(id object, NSError *error) {
    if (error) {
        // Oh noes! report the failure.
    } else if (![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
    } else {
        // W00t! I've got my object
    }
}];

o simplemente

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    // W00t! I've got my object
}];

No es el mejor fragmento de código y el anidamiento empeora

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    [object objectTaskWithCompletion:^(id object2, NSError *error) {
        if (error || ![object validateObject2:&object2 error:&error]) {
            // Oh noes! report the failure.
            return;
        }

        // W00t! I've got object and object 2
    }];
}];

Creo que iré deprimido por un tiempo.

Jeffery Thomas
fuente