Front End

17 jun, 2014

Operações assíncronas: quando escolher Callbacks, Promises, Signals e Events

Publicidade

Algumas vezes, eu me envolvo em discussões sobre desenvolvimento no Twitter e no Facebook. Há alguns meses, acabei enviando um link em uma thread um pouco velha no Twitter, explicando quando usar Promises/Callbacks ou Signals/Events para operações assíncronas. E esse é um tópico que merece uma explicação mais aprofundada.

Callbacks

Callback são geralmente utilizados quando você tem uma operação assíncrona que deveria notificar o caller sobre a realização:

var toppings = {cheese: true, bacon: true};
 
function orderPizza(data, done) {
  // send info to server (which might take a few milliseconds to complete)
  xhr({
    uri: '/order/submit',
    data: data,
    complete: function(status, msg, response) {
      if (status !== 200) {
        // notify about error
        done(new Error(status +': '+ msg));
      }
      // notify about success
      done(null, response);
    }
  });
}
 
// the method "showOrderStatusDialog" will be called after we get the response
// from the server
orderPizza(toppings, showOrderStatusDialog);
 
function showOrderStatusDialog(err, response) {
  if (err) {
    showOrderFailure(err);
  } else {
    showOrderSucces(response);
  }
}

Callbacks são ótimos para casos quando se tem uma ação que gera uma reação imediata (como animação, ajax, processo em batch).

PS: Tecnicamente, um callback é qualquer função que é passada como um argumento para outra função e é executada mais tarde, seja síncrona ou assíncrona. Mas vamos manter o foco em ações assíncronas que requerem uma resposta.

Promises

Promises são uma substituição para Callbacks que ajudam a lidar com a composição de múltiplas operações assíncronas enquanto também lidam com erros de forma mais simples, em alguns casos.

function orderPizza(data) {
  // the xhr in this case returns a promise and handle the error logic
  // internally; it shifts the complexity to the xhr implementation instead
  // of spreading it through your app code
  return xhr({
    uri: '/order/submit',
    data: data
  });
}
 
// first callback of `then` is success, 2nd is failure
orderPizza(toppings).then(showOrderSucces, showOrderFailure);

Há duas coisas realmente legais sobre Promises:

  1. Elas são resolvidas apenas uma vez.
  2. O método then retorna outra Promise, e você pode retornar dados do Callbacl para fazer um map do valor recebido pelo próximo Callback na cadeia.

Como as promises são resolvidas apenas uma vez, podem ser usadas para fazer um caching inteligente ou para casos quando você pode ter múltiplas chamadas tentando executar uma mesma ação (o que deveria acontecer apenas uma vez)

var _cache = {};
function getOrderInfo(id) {
  if (! _cache[id]) {
    _cache[id] = xhr({
      uri: '/order/'+ id
    });
  }
  return _cache[id];
}
 
getOrderInfo('123').then(...);
 
// .... later in the code
 
// since we already asked for the order "123" info it won't do another XHR
// request, it will just use the cached promise and execute callbacks whenever
// the promise is resolved; if it was already resolved it will execute callback
// on the nextTick
getOrderInfo('123').then(...);

E você pode compor múltiplas operações assíncronas.

orderPizza(toppings)
  // if `processStatus` returns a promise the `showOrderSucces` will only be
  // called once the promise is "fullfilled", this makes composition of
  // multiple async operations very easy;
  // if the value returned by `processStatus` is not a promise it will be
  // passed directly to the `showOrderSucces` (similar to Array#map)
  .then(processStatus)
  .then(showOrderSuccess, showOrderFailure);

PS: Há uma proposta para adicionar Promises às especificações do EcmaScript, mas isso tem gerado um monte de discussões – algumas pessoas pensam que esse tipo de abstração deveria estar na “terra do usuário”.

Signals

Signals não são uma substituição para Callbacks/Promises! Eles têm propósitos totalmente diferentes. Signals são usados como uma forma de comunicação entre objetos. Eles são utilizados principalmente quando você precisa reagir a ações que não são responsáveis pelo trigger. Esses eventos geralmente acontecem várias vezes na vida da aplicação e podem ser enviados em intervalos aleatórios.

// you register listeners to each discrete signal and `delivery` object doesn't
// need to know about the existence of your object (or how many objects are
// listening to a specific event)
delivery.leftBuilding.add(userNotifications.dispatched);
delivery.arrivedAtLocation.add(userNotifications.arrival);
delivery.succeed.add(userNotifications.success);
delivery.failed.add(userNotifications.failure);

Qualquer coisa que aconteça múltiplas vezes e/ou que possa ser acionada em intervalos aleatórios deveria ser implementada como Signals/Events.

A vantagem principal de Signals, em comparação com Events, é que eles favorecem composições, e não heranças. Outros benefícios são a descoberta mais alta (é mais fácil identificar que eventos o objeto envia), e ele também evita typos e/ou ouvir o tipo errado de evento; como tentar acessar uma propriedade não-existente gera erros – que geralmente ajuda a mostrar erros mais cedo e simplifica o processo de debug.

Events

EventEmitters são basicamente um Objeto que cria/envia múltiplos tipos de Signals de acordo com a demanda e geralmente usa Strings para definir o nome da mensagem.

A maior vantagem de Events, em comparação com Signals, é que você pode enviar eventos dinâmicos durante o runtime – imagine que você queira um tipo diferente de evento para cada mudança na sua classe Model.

Node.js usa bastante o EventEmitter internamente, mas cria APIs parecidas com callback para abstrair/simplificar o processo, como o método http.createServer:

http
  .createServer(respondToRequest)
  .listen(1337, '127.0.0.1');

Que é exatamente igual a:

var server = http.createServer();
server.on('request', respondToRequest);
server.listen(1337, '127.0.0.1');

Então, mesmo que pareça que http.createServer pegue um Callback feito regularmente (como descrito nos primeiros exemplos), ele é, na verdade, um event listener. Eu não recomendo esse tipo de API, já que à primeira vista o usuário pensaria que o argumento http.createServer seria executado depois da “criação”, e não a cada requisição.

Eu espero que tenha ficado claro quando/como usar esses quatro padrões!

PS: Eu geralmente prefiro Signals a EventEmitter, já que eu acredito que ele tem alguns benefícios melhores. Estou planejando o código e o release da versão 2.0 do js-signals com algumas melhorias, ligeiramente diferente da API e encerrando algumas features ruins. Feedbacks e pedidos de pull são bem-vindos!