Controle de fluxo em javascript. Callback, promises e generators.

Código fonte dos exemplos disponível em: https://github.com/ericholiveira/blog_js_examples/tree/master/flow-control

Mesmo programadores javascript experientes tem problemas em entender a ordem de execução de um programa e saber como mantê-lo claro e legível. Neste post eu vou explicar como utilizar 3 diferentes técnicas para controle de fluxo de códido em javascript, começando pela mais simples (callbacks) e terminando com os famigerados generators. O que (e quando) utilizar cada uma delas fica a seu cargo.

Se você é como eu, precisa ver código para realmente entender o que está acontecendo, então ao longo desse post vamos criar um programa bem simples , a tarefa é ler dois arquivos (file1.txt e file2.txt) e imprimir na console o conteúdo do primeiro concatenado com um separador e o conteúdo do segundo. Em cada um dos casos, mostrarei como fazer um acesso sequencial e como acessar em paralelo.

1- Callbacks (what tha hell!!!)

A forma mais simples de controle de fluxo são os chamados callbacks, essa também é a forma padrão do node para acesso a recursos externos. De maneira geral, callbacks são funções passadas como parâmetro que serão executadas ao fim de uma determinada tarefa (por exemplo, ler um arquivo do disco). Na forma definida como padrão no node, um callback recebe até dois parâmetros, o primeiro é o erro (se ocorrer) e o segundo é o resultado da operação (caso seja executada com sucesso). Então ele tem a forma

function(err,result){/*SOME CODE*/}

Neste primeiro exemplo, primeiro lemos o arquivo file1.txt, e só depois que a operação estiver finalizada a função de callback é chamada, verificamos se houve algum erro, e caso não ocorra nenhum erro, lemos o segundo, ao fim da operação se tudo der certo o resultado é impresso na tela.

var fs = require('fs');
var SEPARATOR = '\n-----------------\n';
fs.readFile('./file1.txt', function(err, file1) {
//Lê o primeiro arquivo
if (err) { // verifica se houve erro
return console.log(err);
} else { //Leu com sucesso
fs.readFile('./file2.txt', function(err, file2) {
//Lê o segundo arquivo
if (err) { // verifica se houve erro
return console.log(err);
}
console.log(file1 + SEPARATOR + file2); //Imprime o resultado
});
}
});

O problema com o primeiro exemplo é que estamos lendo primeiro um arquivo e só então lendo o segundo. Como são operações independentes, podemos ler os dois arquivos e então ao fim das duas operações imprimir o resultado, como no exemplo abaixo:

var fs = require('fs');
var count = 0;
var content1, content2;
count++;
fs.readFile('./file1.txt', function(err, data) {
count--;
if (err) {
return console.log(err);
} else {
content1 = data;
if (count === 0) {
console.log(content1 + SEPARATOR + content2);
}
}
});
count++;
fs.readFile('./file2.txt', function(err, data) {
count--;
if (err) {
return console.log(err);
} else {
content2 = data;
if (count === 0) {
console.log(content1 + SEPARATOR + content2);
}
}
});

Agora as coisas começaram a ficar mais complexas, como não sabemos qual leitura terminará primeiro, temos que ter duas variáveis para armazenar o resultado de cada operação (content1 e content2) e mais uma que é utilizada pra saber se as operações já foram finalizadas (count , note que antes de cada operação eu incremento e decremento ao fim de cada, assim, no momento em que ela voltar a ser zero, significa que todas as operações se encerraram).

Infelizmente, uma pequena alteração fez com que o código ficasse bem menos legível já que eu estou adicionando um monte de código (acumuladores e contador) que não tem nada a ver com a minha regra de negócio (ler e imprimir conteúdo de arquivo). Além disso, no primeiro exemplo, nós temos o que é chamado de callback hell, que nada mais é do que diversos callbacks aninhados, no exemplo ainda é possível entender o código, mas conforme o número de callbacks aninhados vai crescendo se torna muito difícil de ler e muito propenso a erro, já que você pode acabar modificando uma variável de um contexto superior e destruir todo o código.

Para acabar com os callbacks aninhados e preservar uma identação melhor e um código menos suscetível a modificação de outros contextos foram criadas as chamadas Promises

2- Promises

Promises foram criadas com o intuito de facilitar o controle de fluxo. Para que diferentes implementações pudessem interoperar facilmente, foi criado o padrão Promises A+ (infelizmente o jquery não o segue) e neste post estamos falando desse padrão.

Promises são objetos que guardam o resultado de uma operação ASSÍNCRONA, dessa forma eles são facilmente reutilizáveis e combinados. Estes objetos sempre possuem uma função chamada then, esta função recebe como parâmetro uma outra função que será executada ao fim da operação em caso de sucesso. Eles também possuem uma função chamada catch que é o análogo do then em caso de erros, e uma função finally que é chamada independente de sucesso ou falha.

O retorno de then e catch é sempre uma promise com o último resultado. Então nós podemos fazer por exemplo (utilizando a biblioteca https://github.com/petkaantonov/bluebird como implementação de promise):

var Promise = require("bluebird");
//Promise.resolve cria uma promise com o resultado passado como //parâmetro
Promise.resolve(3).then(function(result){
return result+2;// 3+2 será passado para o próximo then
}).then(function(result){
//a função passada para o then pode retornar um objeto ou uma //promise, ele lidará com isso para a gente
return Promise.resolve(result+5);
}).then(console.log);// imprime 10

Elas também lidam com erros em qualquer nível, então alterando um pouco o código temos:

var Promise = require("bluebird");
//Promise.resolve cria uma promise com o resultado passado como //parâmetro
Promise.resolve(3).then(function(result){
throw new Exception();//Um erro inesperado ocorreu
return result+2;// 3+2 será passado para o próximo then
}).then(function(result){
//a função passada para o then pode retornar um objeto ou uma //promise, ele lidará com isso para a gente
return Promise.resolve(result+5);
}).then(console.log).catch(function(err){
//Este código será executado logo após o erro
console.log('Ops, ocorreu um erro');
});

Entendendo como promises funcionam o nosso código original fica bem mais simples, no caso sequencial:

var fs = require('fs');
var _Promise = require('bluebird');
var SEPARATOR = '\n-----------------\n';
_Promise.promisifyAll(fs);
var _file1;
fs.readFileAsync('./file1.txt').then(function(file1) {
_file1 = file1;
return fs.readFileAsync('./file2.txt');
}).then(function(file2){
return _file1 + SEPARATOR + file2;
}).then(console.log).catch(console.log);

promisifyAll é uma função do bluebird que torna todas as funções assíncronas de um objeto em Promises

E no caso paralelo é ainda mais simples:

var fs = require('fs');
var _Promise = require('bluebird');
var SEPARATOR = '\n-----------------\n';
_Promise.promisifyAll(fs);
_Promise.join(
fs.readFileAsync('./file1.txt'),
fs.readFileAsync('./file2.txt')
).then(function(result) {
console.log(result[0] + SEPARATOR + result[1]);
}).catch(console.log);

join é uma função do bluebird para executar múltiplas promises em paralelo e aguardar o resultado de todas

Apesar do ganho notável de legibilidade, este código não se parece muito com um código imperativo tradicional, e é aí que entram os generators.

3- Generators

Generators são uma nova feature do ES6 e são bastante falados. Neste post, vamos nos a ter a como eles podem nos ajudar a escrever um código mais legível. Primeiramente, para que sua função seja um generator ela tem que ser declarada como function* ao invés de function , dentro de um generator você pode utilizar a keyword yield que basicamente para a execução de um generator naquele ponto para seguir em outro momento.

Atenção: Para usar generators no node precisamos da flag

--harmony
Para rodar os exemplos abaixo:
node --harmony generatorSimple.js

Para controle de fluxos assíncronos foi criada a biblioteca co (https://github.com/tj/co), que permite utilizar um generator para “esperar” pelo fim de uma promise, então nossos exemplos passam a ser, no caso sequencial:

var fs = require('fs');
var _Promise = require('bluebird');
var co = require('co');
_Promise.promisifyAll(fs);
co(function*() {
var file1 = yield fs.readFileAsync('./file1.txt');
var file2 = yield fs.readFileAsync('./file2.txt');
console.log(file1 + SEPARATOR + file2);
}).catch(console.log);

Ou em paralelo:

var fs = require('fs');
var _Promise = require('bluebird');
var co = require('co');
_Promise.promisifyAll(fs);
co(function*() {
var file1Prom = fs.readFileAsync('./file1.txt');
var file2Prom = fs.readFileAsync('./file2.txt');
var result = yield _Promise.join(file1Prom,file2Prom);
console.log(result[0] + SEPARATOR + result[1]);
}).catch(console.log);

Agora nós temos um código bem mais simples e legível.

Promises e generators tem muitos outros comportamentos que podem ajudar a simplificar o seu código, o intuito deste post foi mostrar os ganhos de legibilidade em se utilizar essas ferramentas.

Até a próxima.

Like what you read? Give Erich Oliveira a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.