Desenvolvimento

30 jun, 2014

Padrões para programação assíncrona com Promises

Publicidade

Promises são atualmente as melhores ferramentas de que dispomos para a programação assíncrona e parecem ser nossa maior esperança para o futuro previsível, mesmo que estejam se escondendo por trás de geradores ou funções assíncronas. Por enquanto, vamos precisar usar Promises diretamente, por isso vamos aprender algumas técnicas legais para usá-las agora, especialmente quando se lida com várias operações assíncronas, mesmo quando elas acontecem em paralelo ou sequencialmente.

Antes de começarmos

Antes de entrar em detalhes sobre os padrões, vou ter que falar de alguns pontos. Primeiro, estou usando Q para minha implementação de Promises. Também estou usando Underscore ou Lodash (escolha seu favorito; pessoalmente, gosto do Lodash) para coisas tipo map, each e reduce. No código, asyncOperation só representa uma função que recebe um único parâmetro de número, executa uma operação assíncrona de acordo com esse número e retorna uma Promise, enquanto // … representa qualquer código que seja específico para o aplicativo que opera sobre os valores retornados de asyncOperation.

Operações assíncronas paralelas

Primeiro vamos dar uma olhada nas operações paralelas. Isso se refere à obtenção de várias operações assíncronas enfileiradas e em execução ao mesmo tempo. Ao executá-las em paralelo, você pode aumentar significativamente o seu desempenho. Infelizmente, isso nem sempre é possível. Pode ser que você seja obrigado a executar as operações em ordem sequencial, que é o que nós vamos falar na próxima seção.

De qualquer forma, vamos dar uma olhada primeiro na execução das operações assíncronas em paralelo, mas, em seguida, realizar operações síncronas nelas em uma ordem específica, após todas as operações assíncronas terem terminado. Isso te dá um aumento de desempenho das operações paralelas, mas, em seguida, traz tudo de volta para fazer as coisas na ordem certa quando você precisar.

function parallelAsyncSequentialSync (){
    var values = [1,2,3,4];
 
    // Use `map` to create an array of promises by performing
    // `asyncOperation` on each element in the original array.
    // They should happen in parallel.
    var operations = _.map(values, asyncOperation);
 
    // Return a promise so outside code can wait for this code.
    return Q.all(operations).then(function(newValues) {
        // Once all of the operations are finished, we can loop
        // through the results and do something with them
        _.each(newValues, function(value) {
            // ...
        });
 
        // Make sure we return the values we want outside to see
        return newValues;
    });
}

Usamos map para obter todas as nossas operações assíncronas acionadas de imediato, mas, depois, usamos Q.all para esperar que todos eles terminem. Então a gente só tem que executar um loop nos novos valores e fazer quaisquer operações que forem necessárias na ordem original.

Às vezes, a ordem em que nossas operações síncronas são executadas não importa. Nesse caso, pode-se executar cada uma de nossas operações síncronas imediatamente após suas respectivas operações assíncronas terem terminado.

function parallelAsyncUnorderedSync (){
    var values = [1,2,3,4];
 
    // Use `map` to create an array of promises
    var operations = _.map(values, function(value) {
        // return the promise so `operations` is an array of promises.
        return asyncOperation(value).then(function(newValue) {
            // ...
 
            // we want the new values to pass to the outside
            return newValue;
        });
    });
 
    // return a promise so the outside can wait for all operations to finish.
    return Q.all(operations);
}

Para isso, usamos map de novo, mas, em vez de esperar que todas as operações terminem, fornecemos o nosso próprio callback para o map e vamos além. Dentro dele, nós invocamos nossa função assíncrona e então chamamos o then imediatamente, para montar nossa operação síncrona para executar imediatamente após a assíncrona ser concluída.

Operações assíncronas sequenciais

Vamos dar uma olhada em alguns padrões para operações assíncronas sequenciais. Nesse caso, a primeira operação assíncrona deve terminar antes de passar para a próxima. Tenho duas soluções para isso – uma utiliza each e a outra, reduce. Elas são bastante parecidas, mas a versão com each precisa armazenar uma referência à cadeia promise, enquanto a versão com reduce transmite como o memo. Essencialmente, a versão com each só é mais explícita e detalhada, mas ambas fazem a mesma coisa.

 

function sequentialAsyncWithEach (){
    var values = [1,2,3,4];
    var newValues = [];
    var dfd = Q.defer();
    var promise;
 
    dfd.resolve();
    promise = dfd.promise;
 
    _.each(values, function(value) {
        promise = promise.then(function() {
            return asyncOperation(value);
        }).then(function(newValue) {
            // ...
            newValues.push(newValue);
        });
    });
 
    return promise.then(function() {
        return newValues;
    });
}
function sequentialAsyncWithReduce (){
    var values = [1,2,3,4];
    var newValues = [];
    var dfd = Q.defer();
 
    dfd.resolve();
 
    return _.reduce(values, function(memo, value) {
        return memo.then(function() {
            return asyncOperation(value);
        }).then(function(newValue) {
            // ...
            newValues.push(newValue);
        });
    }, dfd.promise).then(function() {
        return newValues;
    });
}

Em cada versão, a gente só encadeia cada operação assíncrona fora da anterior. É irritante que precisamos criar uma Promise “em branco”, que é usado só para iniciar a cadeia, mas é um mal necessário. Além disso, precisamos atribuir explicitamente valores para a array NewValues, o que é outro mal necessário, embora talvez não tão mal assim. Eu pessoalmente acho que a versão com each é um pouco mais fácil de ler graças à sua natureza explícita, mas é uma escolha mais de estilo, e a com reduce funciona perfeitamente para essa situação.

Conclusão

Eu costumava pensar que Promises eram muito diretas e tive dificuldade para encontrar uma razão para usá-las nos callbacks padrões, mas quanto mais preciso delas, mais útil acho que eles são. Porém também acho que são mais complicadas por conta dessas inúmeras maneiras de serem usadas​​, como mostrado acima. Se você não tiver tudo isso bem gravado na sua cabeça, é melhor salvá-las em algum lugar para  tê-los à mão quando você precisar.

Bem, por hoje é só!

***

Artigo traduzido com autorização do autor. Publicado originalmente em http://www.joezimjs.com/javascript/patterns-asynchronous-programming-promises/