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?
design-patterns
objective-c
Jeffery Thomas
la source
la source
Réponses:
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
-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.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:
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:En fait,
completion_t
tel 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:
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.
Lorsqu'une promesse a été résolue, elle ne peut plus changer son état, y compris sa valeur.
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:
où 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».
Dans RXPromise, les blocs de gestionnaire ont la signature suivante:
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,
then
est 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.Ainsi, l'expression
promise.then(success_handler, error_handler);
est une forme abrégée deNous pouvons écrire du code encore plus concis:
Le code indique: «Exécutez doSomethingAsync, quand il réussit, puis exécutez le gestionnaire de réussite».
Ici, le gestionnaire d'erreur est
nil
ce 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é
then
retournera 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 .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»:
De même, lorsque la tâche asynchrone échoue, la promesse retournée sera résolue (c'est-à-dire «rejetée») avec une erreur.
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:
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
,asyncTaskC
etasyncTaskD
successivement - et chaque tâche suivante prend le résultat de la tâche précédente comme entrée: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 :
Cela s'apparente au style synchrone probablement plus familier avec la gestion des exceptions:
Les promesses ont en général d'autres caractéristiques utiles:
Par exemple, ayant une référence à une promesse, via
then
on 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
cancel
message 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.
la source
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.la source
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.
la source
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
ou simplement
Pas le meilleur morceau de code et l'imbrication empire
Je pense que je vais me morfondre un moment.
la source