succès: / échec: blocs vs achèvement: bloc

23

Je vois deux modèles courants de blocs dans Objective-C. L'un est une paire de succès: / échec: blocs, l'autre est un seul achèvement: bloc.

Par exemple, disons que j'ai une tâche qui retournera un objet de manière asynchrone et que cette tâche peut échouer. Le premier motif est -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. Le deuxième motif est -taskWithCompletion:(void (^)(id object, NSError *error))completion.

succès: / échec:

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

achèvement:

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

Quel est le modèle préféré? Quelles sont les forces et les faiblesses? Quand utiliseriez-vous l'un sur l'autre?

Jeffery Thomas
la source
Je suis sûr que Objective-C a une gestion des exceptions avec throw / catch, y a-t-il une raison pour laquelle vous ne pouvez pas utiliser cela?
FrustratedWithFormsDesigner
L'un ou l'autre permet d'enchaîner des appels asynchrones, ce que les exceptions ne vous donnent pas.
Frank Shearar
5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - objc idiomatique n'utilise pas try / catch pour le contrôle de flux.
Ant
1
Veuillez envisager de déplacer votre réponse de la question à une réponse ... après tout, c'est une réponse (et vous pouvez répondre à vos propres questions).
1
J'ai finalement cédé à la pression des pairs et déplacé ma réponse vers une réponse réelle.
Jeffery Thomas

Réponses:

8

Le rappel de fin (opposé à la paire succès / échec) est plus générique. Si vous devez préparer un certain contexte avant de traiter le statut de retour, vous pouvez le faire juste avant la clause "if (object)". En cas de succès / échec, vous devez dupliquer ce code. Cela dépend bien sûr de la sémantique des rappels.


la source
Je ne peux pas commenter la question d'origine ... Les exceptions ne sont pas un contrôle de flux valide dans objective-c (enfin, cacao) et ne doivent pas être utilisées comme telles. L'exception levée doit être interceptée uniquement pour se terminer correctement.
Ouais, je peux voir ça. Si vous -task…pouvez renvoyer l'objet, mais que l'objet n'est pas dans l'état correct, vous aurez toujours besoin de la gestion des erreurs dans la condition de réussite.
Jeffery Thomas
Oui, et si le bloc n'est pas en place, mais est passé en argument à votre contrôleur, vous devez lancer deux blocs. Cela peut être ennuyeux lorsque le rappel doit être passé à travers de nombreuses couches. Vous pouvez toujours le diviser / le recomposer.
Je ne comprends pas comment le gestionnaire d'achèvement est plus générique. L'achèvement transforme essentiellement plusieurs paramètres de méthode en un seul - sous la forme de paramètres de bloc. Aussi, le générique signifie-t-il mieux? Dans MVC, vous avez souvent aussi du code en double dans le contrôleur de vue, c'est un mal nécessaire en raison de la séparation des préoccupations. Je ne pense pas que ce soit une raison pour rester loin de MVC.
Boon
@Boon Une raison pour laquelle je considère le gestionnaire unique comme étant plus générique est pour les cas où vous préféreriez que l'appelé / gestionnaire / bloc détermine lui-même si une opération a réussi ou échoué. Considérez les cas de réussite partielle dans lesquels vous possédez éventuellement un objet avec des données partielles et votre objet d'erreur est une erreur indiquant que toutes les données n'ont pas été renvoyées. Le bloc pourrait examiner les données elles-mêmes et vérifier si elles sont suffisantes. Cela n'est pas possible avec le scénario de rappel binaire succès / échec.
Travis
8

Je dirais que si l'API fournit un gestionnaire d'achèvement ou une paire de blocs de réussite / échec, c'est principalement une question de préférence personnelle.

Les deux approches ont des avantages et des inconvénients, bien qu'il n'y ait que des différences marginales.

Songez qu'il ya aussi d' autres variantes, par exemple lorsque l' un gestionnaire d'achèvement ne peut avoir qu'un un paramètre combinant le résultat final ou une erreur potentielle:

typedef void (^completion_t)(id result);

- (void) taskWithCompletion:(completion_t)completionHandler;

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

Le but de cette signature est qu'un gestionnaire d'achèvement peut être utilisé de manière générique dans d'autres API.

Par exemple, dans Catégorie pour NSArray, il existe une méthode forEachApplyTask:completion:qui appelle séquentiellement une tâche pour chaque objet et rompt la boucle IFF en cas d'erreur. Comme cette méthode est elle-même asynchrone, elle possède également un gestionnaire de complétion:

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

En fait, completion_ttel que défini ci-dessus est suffisamment générique et suffisant pour gérer tous les scénarios.

Cependant, il existe d'autres moyens pour une tâche asynchrone de signaler sa notification d'achèvement au site d'appel:

Promesses

Les promesses, aussi appelé « à terme », « différés » ou « retardée » représentent l' éventuel résultat d'une tâche asynchrone (voir aussi: wiki Futures et promesses ).

Initialement, une promesse est dans l'état «en attente». Autrement dit, sa «valeur» n'est pas encore évaluée et n'est pas encore disponible.

Dans Objective-C, une promesse serait un objet ordinaire qui sera renvoyé d'une méthode asynchrone comme indiqué ci-dessous:

- (Promise*) doSomethingAsync;

! L'état initial d'une promesse est «en attente».

Pendant ce temps, les tâches asynchrones commencent à évaluer son résultat.

Notez également qu'il n'y a pas de gestionnaire d'achèvement. Au lieu de cela, la Promesse fournira un moyen plus puissant où le site d'appel peut obtenir le résultat final de la tâche asynchrone, que nous verrons bientôt.

La tâche asynchrone, qui a créé l'objet de promesse, DOIT finalement «résoudre» sa promesse. Cela signifie qu'une tâche pouvant réussir ou échouer, elle DOIT soit «tenir» une promesse en lui transmettant le résultat évalué, soit elle doit «rejeter» la promesse en lui passant une erreur indiquant la raison de l'échec.

! Une tâche doit finalement tenir sa promesse.

Lorsqu'une promesse a été résolue, elle ne peut plus changer son état, y compris sa valeur.

! Une promesse ne peut être résolue qu'une seule fois .

Une fois qu'une promesse a été résolue, un site d'appel peut obtenir le résultat (qu'il ait échoué ou réussi). La manière dont cela est accompli dépend de l'implémentation de la promesse à l'aide du style synchrone ou asynchrone.

A Promise peut être mis en oeuvre dans un mode synchrone ou asynchrone un qui conduit soit à bloquer , respectivement non-blocage sémantique.

Dans un style synchrone afin de récupérer la valeur de la promesse, un site d'appel utiliserait une méthode qui bloquera le thread actuel jusqu'à ce que la promesse ait été résolue par la tâche asynchrone et que le résultat final soit disponible.

Dans un style asynchrone, le site d'appel enregistre les rappels ou les blocs de gestionnaire qui sont appelés immédiatement après la résolution de la promesse.

Il s'est avéré que le style synchrone présente un certain nombre d'inconvénients importants qui déjouent efficacement les mérites des tâches asynchrones. Un article intéressant sur l'implémentation actuellement imparfaite de «futures» dans la bibliothèque C ++ 11 standard peut être lu ici: Broken promises – C ++ 0x futures .

Comment, dans Objective-C, un site d'appel obtiendrait-il le résultat?

Eh bien, il vaut probablement mieux montrer quelques exemples. Il existe quelques bibliothèques qui implémentent une promesse (voir les liens ci-dessous).

Cependant, pour les prochains extraits de code, j'utiliserai une implémentation particulière d'une bibliothèque Promise, disponible sur GitHub RXPromise . Je suis l'auteur de RXPromise.

Les autres implémentations peuvent avoir une API similaire, mais il peut y avoir de petites et éventuellement subtiles différences de syntaxe. RXPromise est une version Objective-C de la spécification Promise / A + qui définit un standard ouvert pour des implémentations robustes et interopérables de promesses en JavaScript.

Toutes les bibliothèques de promesses répertoriées ci-dessous implémentent le style asynchrone.

Il existe des différences assez importantes entre les différentes implémentations. RXPromise utilise en interne la bibliothèque de répartition, est entièrement sûr pour les threads, extrêmement léger et fournit également un certain nombre de fonctionnalités utiles supplémentaires, telles que l'annulation.

Un site d'appel obtient le résultat final de la tâche asynchrone par le biais de gestionnaires «d'enregistrement». La «spécification Promise / A +» définit la méthode then.

La méthode then

Avec RXPromise, cela ressemble à ceci:

promise.then(successHandler, errorHandler);

successHandler est un bloc qui est appelé lorsque la promesse a été «remplie» et errorHandler est un bloc qui est appelé lorsque la promesse a été «rejetée».

! thenest utilisé pour obtenir le résultat final et pour définir un gestionnaire de réussite ou d'erreur.

Dans RXPromise, les blocs de gestionnaire ont la signature suivante:

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

Le success_handler a un résultat de paramètre qui est évidemment le résultat final de la tâche asynchrone. De même, le gestionnaire d' erreur a une erreur de paramètre qui est l'erreur signalée par la tâche asynchrone lorsqu'elle a échoué.

Les deux blocs ont une valeur de retour. La nature de cette valeur de retour deviendra claire bientôt.

Dans RXPromise, thenest une propriété qui renvoie un bloc. Ce bloc a deux paramètres, le bloc gestionnaire de réussite et le bloc gestionnaire d'erreur. Les gestionnaires doivent être définis par le site d'appel.

! Les gestionnaires doivent être définis par le site d'appel.

Ainsi, l'expression promise.then(success_handler, error_handler);est une forme abrégée de

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

Nous pouvons écrire du code encore plus concis:

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

Le code indique: «Exécutez doSomethingAsync, quand il réussit, puis exécutez le gestionnaire de réussite».

Ici, le gestionnaire d'erreur est nilce qui signifie qu'en cas d'erreur, il ne sera pas traité dans cette promesse.

Un autre fait important est que l'appel du bloc renvoyé par la propriété thenretournera une promesse:

! then(...)retourne une promesse

Lors de l'appel du bloc renvoyé par la propriété then, le «récepteur» renvoie une nouvelle promesse, une promesse enfant . Le récepteur devient la promesse parentale .

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

Qu'est-ce que ça veut dire?

Eh bien, de ce fait, nous pouvons «enchaîner» des tâches asynchrones qui sont exécutées de manière séquentielle.

En outre, la valeur de retour de l'un ou l'autre gestionnaire deviendra la «valeur» de la promesse retournée. Donc, si la tâche réussit avec le résultat final @ «OK», la promesse retournée sera «résolue» (c'est-à-dire «remplie») avec la valeur @ «OK»:

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

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

De même, lorsque la tâche asynchrone échoue, la promesse retournée sera résolue (c'est-à-dire «rejetée») avec une erreur.

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

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

Le gestionnaire peut également retourner une autre promesse. Par exemple, lorsque ce gestionnaire exécute une autre tâche asynchrone. Avec ce mécanisme, nous pouvons «chaîner» des tâches asynchrones:

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

! La valeur de retour d'un bloc de gestionnaire devient la valeur de la promesse enfant.

S'il n'y a pas de promesse enfant, la valeur de retour n'a aucun effet.

Un exemple plus complexe:

Ici, nous exécutons asyncTaskA, asyncTaskB, asyncTaskCet asyncTaskD successivement - et chaque tâche suivante prend le résultat de la tâche précédente comme entrée:

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

Une telle «chaîne» est également appelée «continuation».

La gestion des erreurs

Les promesses facilitent particulièrement la gestion des erreurs. Les erreurs seront «transmises» du parent à l'enfant s'il n'y a pas de gestionnaire d'erreurs défini dans la promesse du parent. L'erreur sera transmise vers le haut de la chaîne jusqu'à ce qu'un enfant la gère. Ainsi, ayant la chaîne ci-dessus, nous pouvons implémenter la gestion des erreurs simplement en ajoutant une autre «continuation» qui traite d'une erreur potentielle qui peut se produire n'importe où ci - dessus :

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

Cela s'apparente au style synchrone probablement plus familier avec la gestion des exceptions:

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

Les promesses ont en général d'autres caractéristiques utiles:

Par exemple, ayant une référence à une promesse, via thenon peut "enregistrer" autant de gestionnaires que souhaité. Dans RXPromise, l'enregistrement des gestionnaires peut se produire à tout moment et à partir de n'importe quel thread car il est entièrement thread-safe.

RXPromise a quelques fonctionnalités fonctionnelles plus utiles, non requises par la spécification Promise / A +. L'une est "l'annulation".

Il s'est avéré que «l'annulation» est une caractéristique inestimable et importante. Par exemple, un site d'appel détenant une référence à une promesse peut lui envoyer le cancelmessage afin d'indiquer qu'il n'est plus intéressé par le résultat final.

Imaginez simplement une tâche asynchrone qui charge une image à partir du Web et qui doit être affichée dans un contrôleur de vue. Si l'utilisateur s'éloigne du contrôleur de vue actuel, le développeur peut implémenter du code qui envoie un message d'annulation à l' imagePromise , qui à son tour déclenche le gestionnaire d'erreurs défini par l'opération de demande HTTP où la demande sera annulée.

Dans RXPromise, un message d'annulation ne sera transmis que d'un parent à ses enfants, mais pas l'inverse. Autrement dit, une promesse «racine» annulera toutes les promesses d'enfants. Mais une promesse d'enfant n'annulera que la «branche» où se trouve le parent. Le message d'annulation sera également transmis aux enfants si une promesse a déjà été résolue.

Une tâche asynchrone peut elle - même enregistrer le gestionnaire pour sa propre promesse et ainsi détecter quand quelqu'un d'autre l'a annulée. Il peut alors cesser prématurément d'effectuer une tâche éventuellement longue et coûteuse.

Voici quelques autres implémentations de Promises in Objective-C trouvées sur 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

et ma propre implémentation: RXPromise .

Cette liste n'est probablement pas complète!

Lorsque vous choisissez une troisième bibliothèque pour votre projet, veuillez vérifier attentivement si la mise en œuvre de la bibliothèque respecte les conditions requises énumérées ci-dessous:

  • Une bibliothèque de promesses fiable DOIT être sûre pour les threads!

    Il s'agit du traitement asynchrone, et nous voulons utiliser plusieurs processeurs et exécuter simultanément sur différents threads dans la mesure du possible. Attention, la plupart des implémentations ne sont pas thread-safe!

  • Les gestionnaires DOIVENT être appelés de manière asynchrone, en respectant le site d'appel! Toujours et quoi qu'il arrive!

    Toute implémentation décente doit également suivre un modèle très strict lors de l'appel des fonctions asynchrones. De nombreux implémenteurs ont tendance à "optimiser" le cas où un gestionnaire sera appelé de manière synchrone lorsque la promesse est déjà résolue lorsque le gestionnaire sera enregistré. Cela peut provoquer toutes sortes de problèmes. Voir Ne pas libérer Zalgo! .

  • Il devrait également y avoir un mécanisme pour annuler une promesse.

    La possibilité d'annuler une tâche asynchrone devient souvent une exigence de haute priorité dans l'analyse des exigences. Sinon, il est certain qu'une demande d'amélioration sera déposée par un utilisateur un peu plus tard après la sortie de l'application. La raison doit être évidente: toute tâche qui peut se bloquer ou prendre trop de temps à terminer, doit être annulable par l'utilisateur ou par un timeout. Une bibliothèque de promesses décentes devrait prendre en charge l'annulation.

CouchDeveloper
la source
1
Cela obtient le prix de la non-réponse la plus longue de tous les temps. But A for effort :-)
Traveling Man
3

Je me rends compte que c'est une vieille question mais je dois y répondre car ma réponse est différente des autres.

Pour ceux qui disent que c'est une question de préférence personnelle, je dois être en désaccord. Il y a une bonne raison logique de préférer l'un à l'autre ...

Dans le cas de l'achèvement, votre bloc se voit remettre deux objets, l'un représente le succès tandis que l'autre représente l'échec ... Alors, que faites-vous si les deux sont nuls? Que faites-vous si les deux ont une valeur? Ce sont des questions qui peuvent être évitées au moment de la compilation et en tant que telles, elles devraient l'être. Vous évitez ces questions en ayant deux blocs distincts.

Le fait d'avoir des blocs de réussite et d'échec séparés rend votre code statiquement vérifiable.


Notez que les choses changent avec Swift. Dans celui-ci, nous pouvons implémenter la notion d' Eitherénumération de sorte que le bloc d'achèvement unique soit garanti d'avoir un objet ou une erreur, et doit en avoir exactement un. Donc, pour Swift, un seul bloc est préférable.

Daniel T.
la source
1

Je pense que ça va finir par être une préférence personnelle ...

Mais je préfère les blocs séparés succès / échec. J'aime séparer la logique de réussite / échec. Si vous aviez des succès / échecs imbriqués, vous vous retrouveriez avec quelque chose qui serait plus lisible (à mon avis du moins).

Comme exemple relativement extrême d'une telle imbrication, voici quelques rubis montrant ce modèle.

Frank Shearar
la source
1
J'ai vu des chaînes imbriquées des deux. Je pense qu'ils ont tous les deux l'air terrible, mais c'est mon opinion personnelle.
Jeffery Thomas
1
Mais comment pourriez-vous enchaîner les appels asynchrones autrement?
Frank Shearar
Je ne connais pas l'homme… je ne sais pas. Une partie de la raison pour laquelle je demande est parce que je n'aime pas à quoi ressemble mon code asynchrone.
Jeffery Thomas
Sûr. Vous finissez par écrire votre code dans un style passant par la suite, ce qui n'est pas très surprenant. (Haskell a sa notation do pour exactement cette raison: vous permettre d'écrire dans un style ostensiblement direct.)
Frank Shearar
Vous pouvez être intéressé par cette implémentation d'ObjC Promises: github.com/couchdeveloper/RXPromise
e1985
0

Cela ressemble à un copout complet, mais je ne pense pas qu'il y ait une bonne réponse ici. Je suis allé avec le bloc d'achèvement simplement parce que la gestion des erreurs doit encore être effectuée dans la condition de réussite lors de l'utilisation des blocs de réussite / échec.

Je pense que le code final ressemblera à quelque chose

[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
    }
}];

ou simplement

[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
}];

Pas le meilleur morceau de code et l'imbrication empire

[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
    }];
}];

Je pense que je vais me morfondre un moment.

Jeffery Thomas
la source