sucesso: / falha: blocos vs conclusão: bloco

23

Eu vejo dois padrões comuns para blocos no Objective-C. Um é um par de sucesso: / falha: blocos, o outro é uma única conclusão: bloco.

Por exemplo, digamos que eu tenho uma tarefa que retornará um objeto de forma assíncrona e essa tarefa poderá falhar. O primeiro padrão é -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. O segundo padrão é -taskWithCompletion:(void (^)(id object, NSError *error))completion.

Não houve sucesso:

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

conclusão:

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

Qual é o padrão preferido? Quais são os pontos fortes e fracos? Quando você usaria um sobre o outro?

Jeffery Thomas
fonte
Tenho certeza de que o Objective-C possui tratamento de exceção com throw / catch, existe algum motivo para você não usar isso?
FrustratedWithFormsDesigner
Qualquer um deles permite encadear chamadas assíncronas, que exceções não oferecem.
precisa saber é o seguinte
5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - objc idiomático não usa try / catch para controle de fluxo.
Ant
1
Considere mover sua resposta da pergunta para uma resposta ... afinal, é uma resposta (e você pode responder suas próprias perguntas).
1
Finalmente cedi à pressão dos colegas e mudei minha resposta para uma resposta real.
Jeffery Thomas

Respostas:

8

O retorno de chamada de conclusão (em oposição ao par de sucesso / falha) é mais genérico. Se você precisar preparar algum contexto antes de lidar com o status de retorno, poderá fazê-lo antes da cláusula "if (object)". No caso de sucesso / falha, você deve duplicar esse código. Isso depende da semântica de retorno de chamada, é claro.


fonte
Não posso comentar sobre a pergunta original ... As exceções não são um controle de fluxo válido no objetivo-c (bem, cacau) e não devem ser usadas como tal. A exceção lançada deve ser capturada apenas para terminar normalmente.
Sim, eu posso ver isso. Se -task…pudesse retornar o objeto, mas o objeto não estiver no estado correto, você ainda precisaria de tratamento de erros na condição de sucesso.
Jeffery Thomas
Sim, e se o bloco não estiver no lugar, mas for passado como argumento para o seu controlador, você deverá lançar dois blocos ao redor. Isso pode ser entediante quando o retorno de chamada precisa ser passado por várias camadas. Você sempre pode dividir / compor de volta.
Não entendo como o manipulador de conclusão é mais genérico. A conclusão basicamente transforma vários parâmetros de método em um - na forma de parâmetros de bloco. Além disso, genérico significa melhor? No MVC, muitas vezes você também tem código duplicado no controlador de exibição, que é um mal necessário devido à separação de preocupações. Eu não acho que essa seja uma razão para ficar longe do MVC.
Boon
@Boon Uma razão pela qual eu vejo o manipulador único como sendo mais genérico é nos casos em que você prefere que o receptor / manipulador / bloco determine se uma operação foi bem-sucedida ou falhou. Considere casos de sucesso parciais em que você possivelmente tenha um objeto com dados parciais e seu objeto de erro seja um erro indicando que nem todos os dados foram retornados. O bloco pode examinar os dados em si e verificar se são suficientes. Isso não é possível no cenário de retorno de chamada binário com êxito / falha.
Travis
8

Eu diria que, se a API fornece um manipulador de conclusão ou um par de blocos de sucesso / falha, é principalmente uma questão de preferência pessoal.

Ambas as abordagens têm prós e contras, embora haja apenas diferenças marginais.

Considere-se que existem também outras variantes, por exemplo, onde o um manipulador de conclusão pode ter apenas um parâmetro combinando o eventual resultado ou um erro 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 {
        ...
    }
}]; 

O objetivo desta assinatura é que um manipulador de conclusão possa ser usado genericamente em outras APIs.

Por exemplo, em Categoria para NSArray, existe um método forEachApplyTask:completion:que chama seqüencialmente uma tarefa para cada objeto e quebra o loop IFF, houve um erro. Como esse método também é assíncrono, ele também possui um manipulador de conclusão:

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

De fato, completion_tconforme definido acima, é genérico e suficiente para lidar com todos os cenários.

No entanto, existem outros meios para uma tarefa assíncrona sinalizar sua notificação de conclusão ao site de chamada:

Promessas

Promessas, também chamadas de "Futuros", "Adiadas" ou "Atrasadas" representam o resultado final de uma tarefa assíncrona (consulte também: wiki Futuros e promessas ).

Inicialmente, uma promessa está no estado "pendente". Ou seja, seu "valor" ainda não foi avaliado e ainda não está disponível.

No Objective-C, uma Promessa seria um objeto comum que será retornado de um método assíncrono, como mostrado abaixo:

- (Promise*) doSomethingAsync;

! O estado inicial de uma promessa está "pendente".

Enquanto isso, as tarefas assíncronas começam a avaliar seu resultado.

Observe também que não há manipulador de conclusão. Em vez disso, a Promessa fornecerá um meio mais poderoso para que o site de chamadas possa obter o resultado final da tarefa assíncrona, que veremos em breve.

A tarefa assíncrona, que criou o objeto de promessa, DEVE eventualmente "resolver" sua promessa. Isso significa que, uma vez que uma tarefa pode ter êxito ou falhar, DEVE "cumprir" uma promessa passando o resultado avaliado ou DEVE "rejeitar" a promessa passando um erro indicando o motivo da falha.

! Uma tarefa deve eventualmente resolver sua promessa.

Quando uma promessa é resolvida, ela não pode mais mudar seu estado, incluindo seu valor.

! Uma promessa pode ser resolvida apenas uma vez .

Depois que uma promessa é resolvida, um site de chamada pode obter o resultado (se falhou ou teve êxito). Como isso é feito depende se a promessa é implementada usando o estilo síncrono ou assíncrono.

A Promise pode ser implementado em um síncrono ou assíncrono um modelo que leva a qualquer bloqueio , respectivamente, sem bloqueio semântica.

Em um estilo síncrono para recuperar o valor da promessa, um site de chamada usaria um método que bloqueará o encadeamento atual até que a promessa tenha sido resolvida pela tarefa assíncrona e o resultado final esteja disponível.

Em um estilo assíncrono, o site de chamada registraria retornos de chamada ou blocos de manipulador que são chamados imediatamente após a promessa ter sido resolvida.

Verificou-se que o estilo síncrono tem várias desvantagens significativas que efetivamente derrotam os méritos das tarefas assíncronas. Um artigo interessante sobre a implementação atualmente incorreta de "futuros" na lib padrão do C ++ 11 pode ser lida aqui: Promessas quebradas - futuros do C ++ 0x .

Como, no Objective-C, um site de chamadas obteria o resultado?

Bem, provavelmente é melhor mostrar alguns exemplos. Existem algumas bibliotecas que implementam uma promessa (veja os links abaixo).

No entanto, para os próximos trechos de código, usarei uma implementação específica de uma biblioteca Promise, disponível no GitHub RXPromise . Eu sou o autor de RXPromise.

As outras implementações podem ter uma API semelhante, mas pode haver diferenças pequenas e possivelmente sutis na sintaxe. RXPromise é uma versão Objective-C da especificação Promise / A + que define um padrão aberto para implementações robustas e interoperáveis ​​de promessas em JavaScript.

Todas as bibliotecas promissoras listadas abaixo implementam o estilo assíncrono.

Existem diferenças bastante significativas entre as diferentes implementações. O RXPromise utiliza internamente a biblioteca de despacho, é totalmente seguro para threads, extremamente leve e também fornece vários recursos úteis adicionais, como cancelamento.

Um site de chamada obtém o resultado final da tarefa assíncrona por meio de "registradores" de manipuladores. A "especificação Promise / A +" define o método then.

O método then

Com o RXPromise, tem a seguinte aparência:

promise.then(successHandler, errorHandler);

onde successHandler é um bloco que é chamado quando a promessa é "cumprida" e errorHandler é um bloco que é chamado quando a promessa é "rejeitada".

! thené usado para obter o resultado final e definir um manipulador de sucesso ou erro.

No RXPromise, os blocos manipuladores têm a seguinte assinatura:

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

O success_handler possui um resultado de parâmetro que é obviamente o resultado final da tarefa assíncrona. Da mesma forma, o error_handler possui um erro de parâmetro, que é o erro relatado pela tarefa assíncrona quando falhou.

Ambos os blocos têm um valor de retorno. O significado desse valor de retorno ficará claro em breve.

No RXPromise, thené uma propriedade que retorna um bloco. Este bloco possui dois parâmetros, o bloco manipulador de sucesso e o bloco manipulador de erro. Os manipuladores devem ser definidos pelo site de chamada.

! Os manipuladores devem ser definidos pelo site de chamada.

Portanto, a expressão promise.then(success_handler, error_handler);é uma forma curta de

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

Podemos escrever um código ainda mais conciso:

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

O código diz: "Execute doSomethingAsync, quando for bem-sucedido, depois execute o manipulador de sucesso".

Aqui, o manipulador de erros é o nilque significa que, em caso de erro, ele não será tratado nesta promessa.

Outro fato importante é que chamar o bloco retornado da propriedade thenretornará uma promessa:

! then(...)retorna uma promessa

Ao chamar o bloco retornado da propriedade then, o "destinatário" retorna uma nova promessa, uma promessa filho . O receptor se torna a promessa dos pais .

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

O que isso significa?

Bem, devido a isso, podemos "encadear" tarefas assíncronas que efetivamente são executadas sequencialmente.

Além disso, o valor de retorno de qualquer manipulador se tornará o "valor" da promessa retornada. Portanto, se a tarefa tiver êxito com o resultado final @ “OK”, a promessa retornada será “resolvida” (que é “cumprida”) com o valor @ “OK”:

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

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

Da mesma forma, quando a tarefa assíncrona falhar, a promessa retornada será resolvida (que é "rejeitada") com um erro.

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

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

O manipulador também pode retornar outra promessa. Por exemplo, quando esse manipulador executa outra tarefa assíncrona. Com esse mecanismo, podemos "encadear" tarefas assíncronas:

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

! O valor de retorno de um bloco manipulador se torna o valor da promessa filho.

Se não houver promessa filho, o valor de retorno não terá efeito.

Um exemplo mais complexo:

Aqui, nós executamos asyncTaskA, asyncTaskB, asyncTaskCe asyncTaskD sequencialmente - e cada tarefa subseqüente leva o resultado da tarefa anterior 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);

Essa "cadeia" também é chamada de "continuação".

Tratamento de erros

As promessas facilitam especialmente o manuseio de erros. Os erros serão "encaminhados" do pai para o filho se não houver um manipulador de erros definido na promessa do pai. O erro será encaminhado pela cadeia até que uma criança lide com isso. Assim, tendo a cadeia acima, podemos implementar o tratamento de erros apenas adicionando outra “continuação” que lida com um erro em potencial que pode ocorrer em qualquer lugar acima :

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

Isso é semelhante ao estilo síncrono provavelmente mais familiar com o tratamento de exceções:

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

As promessas em geral têm outros recursos úteis:

Por exemplo, tendo uma referência a uma promessa, thené possível "registrar" quantos manipuladores desejar. No RXPromise, os manipuladores de registro podem ocorrer a qualquer momento e a partir de qualquer encadeamento, pois é totalmente seguro para encadeamento.

O RXPromise possui alguns recursos funcionais mais úteis, não exigidos pela especificação Promise / A +. Um é "cancelamento".

Descobriu-se que o "cancelamento" é uma característica valiosa e importante. Por exemplo, um site de chamada com uma referência a uma promessa pode enviar a cancelmensagem para indicar que não está mais interessado no resultado final.

Imagine uma tarefa assíncrona que carrega uma imagem da web e que deve ser exibida em um controlador de exibição. Se o usuário se afastar do controlador de exibição atual, o desenvolvedor poderá implementar o código que envia uma mensagem de cancelamento para o imagePromise , que, por sua vez, aciona o manipulador de erros definido pela Operação de Solicitação HTTP, onde a solicitação será cancelada.

No RXPromise, uma mensagem de cancelamento será encaminhada apenas de um pai para seus filhos, mas não vice-versa. Ou seja, uma promessa "raiz" cancelará todas as promessas de crianças. Mas uma promessa infantil só cancelará o “ramo” onde é o pai. A mensagem de cancelamento também será encaminhada às crianças se uma promessa já tiver sido resolvida.

Uma tarefa assíncrona pode -se registar manipulador para a sua própria promessa, e, assim, pode detectar quando alguém o cancelou. Pode então parar prematuramente de executar uma tarefa possivelmente longa e cara.

Aqui estão algumas outras implementações de Promises no Objective-C encontradas no 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

e minha própria implementação: RXPromise .

Esta lista provavelmente não está completa!

Ao escolher uma terceira biblioteca para o seu projeto, verifique cuidadosamente se a implementação da biblioteca segue os pré-requisitos listados abaixo:

  • Uma biblioteca de promessas confiável DEVE ser segura para threads!

    É tudo sobre processamento assíncrono, e queremos utilizar várias CPUs e executar em diferentes threads simultaneamente, sempre que possível. Tenha cuidado, a maioria das implementações não é segura para threads!

  • Os manipuladores devem ser chamados de forma assíncrona, no que diz respeito ao local da chamada! Sempre e não importa o que aconteça!

    Qualquer implementação decente também deve seguir um padrão muito rigoroso ao chamar as funções assíncronas. Muitos implementadores tendem a "otimizar" o caso, em que um manipulador será chamado de forma síncrona quando a promessa já estiver resolvida quando o manipulador será registrado. Isso pode causar todos os tipos de problemas. Consulte Não liberte o Zalgo! .

  • Também deve haver um mecanismo para cancelar uma promessa.

    A possibilidade de cancelar uma tarefa assíncrona geralmente se torna um requisito com alta prioridade na análise de requisitos. Caso contrário, com certeza haverá uma solicitação de aprimoramento do usuário algum tempo depois após o lançamento do aplicativo. O motivo deve ser óbvio: qualquer tarefa que possa parar ou demorar muito para terminar deve ser cancelável pelo usuário ou por um tempo limite. Uma biblioteca de promessas decente deve apoiar o cancelamento.

CouchDeveloper
fonte
1
Isso recebe o prêmio pela maior falta de resposta de todos os tempos. Mas A para o esforço :-)
Traveling Man
3

Sei que essa é uma pergunta antiga, mas preciso respondê-la porque minha resposta é diferente das demais.

Para aqueles que dizem que é uma questão de preferência pessoal, tenho que discordar. Existe uma boa razão lógica para preferir um ao outro ...

No caso de conclusão, seu bloco recebe dois objetos, um representa sucesso enquanto o outro representa falha ... Então, o que você faz se ambos são nulos? O que você faz se ambos têm um valor? Essas são perguntas que podem ser evitadas no momento da compilação e, como tal, deveriam ser. Você evita essas perguntas tendo dois blocos separados.

Ter blocos de sucesso e falha separados torna seu código estaticamente verificável.


Note que as coisas mudam com Swift. Nele, podemos implementar a noção de uma Eitherenumeração para garantir que o único bloco de conclusão tenha um objeto ou um erro e tenha exatamente um deles. Portanto, para Swift, um único bloco é melhor.

Daniel T.
fonte
1

Eu suspeito que vai acabar sendo uma preferência pessoal ...

Mas eu prefiro os blocos separados de sucesso / falha. Eu gosto de separar a lógica do sucesso / falha. Se você tivesse aninhado sucesso / fracassos, acabaria com algo que seria mais legível (na minha opinião pelo menos).

Como um exemplo relativamente extremo desse aninhamento, aqui está um Ruby mostrando esse padrão.

Frank Shearar
fonte
1
Eu vi cadeias aninhadas de ambos. Acho que os dois parecem terríveis, mas essa é minha opinião pessoal.
Jeffery Thomas
1
Mas de que outra forma você poderia encadear chamadas assíncronas?
precisa saber é o seguinte
Eu não sei cara ... eu não sei. Parte do motivo pelo qual estou perguntando é porque não gosto da aparência de nenhum código assíncrono.
Jeffery Thomas
Certo. Você acaba escrevendo seu código no estilo de passagem de continuação, o que não é muito surpreendente. (Haskell tem a sua notação de fazer exatamente isso: que lhe permite escrever em um estilo aparentemente direta.)
Frank Shearar
Você pode estar interessado nesta implementação Promises ObjC: github.com/couchdeveloper/RXPromise
e1985
0

Parece uma cópia completa, mas não acho que haja uma resposta certa aqui. Fui com o bloco de conclusão simplesmente porque o tratamento de erros ainda precisa ser feito na condição de sucesso ao usar blocos de sucesso / falha.

Eu acho que o código final será algo como

[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 simplesmente

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

Não é o melhor pedaço de código e o aninhamento fica pior

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

Acho que vou ficar deprimido por um tempo.

Jeffery Thomas
fonte