quarta-feira, 21 de maio de 2014

Lidando com operações assíncronas no Javascript







Se você começou a tentar escrever código Javascript para uso no Node.js, então já se deparou com o problema do “Callback Hell”, certo? Veremos aqui algumas maneiras de contornar isso, usando padrões de programação Javascript.









Ô lôco, meu!



Certamente, é como você deve ter se sentido ao tentar escrever (ou entender) um código Javascript que executa mais de uma função assíncrona. Deve ter ficado mais ou menos desse jeito:



fs.readdir(source, function(err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function(filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function(err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function(width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height 
         + 'x' + height)
            this.resize(width, height).write(destination + 'w' 
         + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})


É quando temos operações assíncronas encadeadas em nosso código de evento Javascript. É complicado de programar e mais complicado ainda de manter.

Cara, Java e Javascript são completamente diferentes... Além disso, a maior parte da programação Java e .NET é para código síncrono, no qual emitimos um comando de I/O e nosso programa fica bloqueado, aguardando o término da operação.

O Node.js, como a maioria das plataformas C10K (http://www.obomprogramador.com/2014/04/c10k-e-open-web-moldando-o-futuro-das.html), é baseado em I/O não bloqueante, ou seja, você solicita uma operação e informa quais são suas funções a serem invocadas, de acordo com os eventos. Por exemplo:

fs.open(path, flags, [mode], callback)

ou 

req.on('connect', function(res, socket, head) {
    console.log('got connected!');

    // make a request over an HTTP tunnel
    socket.write('GET / HTTP/1.1\r\n' +
                 'Host: www.google.com:80\r\n' +
                 'Connection: close\r\n' +
                 '\r\n');
    socket.on('data', function(chunk) {
      console.log(chunk.toString());
    });
    socket.on('end', function() {
      proxy.close();
    });
  });


O que acontece com os callbacks?

O Node.js tem um loop de eventos. Ao receber um request de um Cliente, o único thread do Node.js executa o código. Normalmente, o código de tratamento vai iniciar alguma operação de I/O, cujo processamento é executado por funções informadas como “callback”. É um conceito semelhante ao “function pointer” da linguagem “C”.

Ao receber um pedido de I/O, o node envia a solicitação a um “pool” interno de threads, para execução assíncrona, ligando o “callback” informado a um evento. Quando o evento ocorre, ele sabe qual “callback” deverá informar. Quando o loop de eventos chegar ao ponto, ele já saberá o que ocorreu e qual função deverá ser executada.

Dependendo do que quisermos fazer, é bem possível que tenhamos que fazer várias operações assíncronas, por exemplo (abstraindo o código javascript):

verificar.se.existe.no.banco (
   caso.exista {
      invocar.webservice(
         caso.sucesso {
         }
         caso.erro {
         }
      )
   }
   caso.nao.exista {
      retornar erro;
   }
)


Ou seja, encadeamos callbacks dentro de callbacks, o que gera exatamente o “Callback Hell”. Aliás, existe um site muito bom, que dá conselhos sobre como evitar isso:

http://callbackhell.com/

Alternativas



É claro que existem alternativas para tratar operações assíncronas. Vamos apresentar as mais comuns para você.

Async


O “async” (https://github.com/caolan/async) é um módulo NPM que implementa alguns padrões para lidar com operações assíncronas. Vou mostrar algumas funções desse módulo.

Problema 1: Você tem um vetor de valores e quer executar determinada função, em paralelo, para cada um desses valores, e, quando todas terminarem, quer fazer alguma coisa com o resultado. Para isso, tem o “async.map”:

async.map(['file1','file2','file3'], fs.stat, 
   function(err, results){
    // results is now an array of stats for each file
});


Esse comando vai invocar a função “fs.stat”, que retorna informações sobre cada arquivo. O “map” vai invocar a função para cada arquivo do vetor, armazenando em um segundo vetor os resultados. Ao final, vai invocar nosso “callback” passando um vetor de resultados (“results”).

Existem mais de 20 funções de controle de fluxo, como o “map”. Porém, uma das mais populares atende ao problema: “Como encadear resultados de callbacks, de maneira fácil?” A função “waterfall” serve como uma luva:

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
      // arg1 now equals 'one' and arg2 now equals 'two'
        callback(null, 'three');
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
   // result now equals 'done'    
});


Invoca cada função dentro do vetor. Cada uma recebe um argumento e um callback, que deve invocar quando sua função é completada. Cada função executa e passa seu resultado como parâmetro para a próxima função. Assim, temos a serialização de funções assíncronas, só que usando o “loop de eventos”.

Eis um Gist meu com um código de exemplo: https://gist.github.com/cleuton/a905354688db729a5c1f



Promises


De todas as doideiras que eu já vi em programação, essa é uma das mais “cheiradas”, porém, funciona muito bem. Promises é um padrão para encapsulamento de operações assíncronas, proposto no CommonJS (http://wiki.commonjs.org/wiki/Promises/A). Basicamente, uma “promise” é o resultado de uma operação assíncrona, que pode ser: “cumprida”, “não cumprida” ou “falha”.
Uma “promise” deve implementar a função “then”:

then (callbackCumprida, callbackFalha, callbackProgresso)


“callbackCumprida” : Função a ser invocada quando a promessa concluir com sucesso seu trabalho;
“callbackFala” : Função a ser invocada quando a promessa apresentar falha;
“callbackProgresso” : Função a ser invocada para relatar o progresso da tarefa da promessa;

Os callbacks não são obrigatórios.

E o mais importante de uma promessa é que o resultado do “then” deve retornar uma promessa também. Isso permite “encadear” execuções de operações assíncronas. Se o resultado de uma promessa é definitivo, então você pode usar o “done” (ainda não é padronizado), que não retorna uma promessa.

Troço doido, não?

Tenho um Gist com um exemplo para mostrar a você: https://gist.github.com/cleuton/4ce9912796a2e88bdff3



A “promise” principal retorna uma nova “promise” quando bem concluída (variável “mostrar”):

   var promise = new Promise(function (resolve, reject) {
        console.log('Entrou no comando da promise '); 
        Request('http://www.google.com', 
      function (err, resp, body) {
          if (err) reject({"response" : resp, "body" : body});
          else resolve({"response" : resp, "body" : body});
        });
   })
   .then(mostrar)
   .then(aprovada, rejeitada)


O callback da variável “mostrar” é invocado quando o retorno é bem sucedido. Como ele também retorna uma “promise”, então podemos encadear outra operação assíncrona:

   var mostrar = function(httpresponse) {
      console.log('Mostrar !!! status: ' 
         + httpresponse.response.statusCode);            
      return new Promise(function(resolve,reject) {
         resolve(httpresponse);
      });
   };


Por mais “cheirado” que isso pareça, funciona muito bem. Quando invocamos os dois métodos “then()” em sequência, estamos invocando a primeira “promise” e, se tudo correr bem, invocamos a segunda. Tudo rodando dentro do “loop de eventos”.

Porém, nem tudo são flores... A proposta original de “Promises” (http://wiki.commonjs.org/wiki/Promises/A) foi modificada por outra fonte (http://promises-aplus.github.io/promises-spec/), e foi implementada de forma diferente em várias bibliotecas (http://joseoncode.com/2013/05/23/promises-a-plus/).

Conclusão



Cara, na boa, se você vai entrar nessa “onda” de Node.js, deve abandonar seus conceitos de vários anos em plataformas diferenes, como Java, Java EE e .Net. Esse modelo de “I/O” não bloqueante e loop de eventos pode deixá-lo completamente maluco, afetando a sua produtividade. Para evitar isso, abra sua mente e veja como as pessoas estão trabalhando.

Eu fiz uma enquete informal via Twitter, em vários grupos de Node.js que participo, e a maioria dos desenvolvedores declarou que usa uma das duas soluções (“async” e “promises”), e que até combinam as duas.

Pelo que tenho visto, a galera que começou na outra “ponta” do desenvolvimento Web, ou seja, criando páginas e scripts com jQuery, tem a mente mas aberta a esses novos padrões para Javascript no Servidor.