Front End

28 ago, 2013

Arquiteturas assíncronas de JavaScript: Events vs Promises

Publicidade

Eu não passo uma única semana sem ler outro artigo falando de Promises. Eu não estou falando sobre quando você promete a seu filho que você vai estar no seu jogo de beisebol. Estou falando de um conceito JavaScript que o torna mais simples para reagir a ações assíncronas sem recuar  dez níveis quando você precisa executar uma ação assíncrona após a outra. Enquanto trabalhava em uma aplicação Backbone, eu tentei usar meus promises em meus principais processos assíncronos, e eu não tenho certeza de que ele acompanha a arquitetura event hub anterior . Vamos comparar!

Antes que eu entre no porquê de eu preferir o event hub, pelo menos para o meu próprio aplicativo, eu gostaria de falar de cada uma das metodologias para que você possa compreendê-las melhor, apenas no caso de você não ter ouvido falar muito sobre elas.

Promises e deferred object

Em vez de criarem uma função que permita enviar um callback que é executado quando uma ação termina, a função retorna um objeto promise. Depois dele, você pode chamar algo como done e enviar um callback para ele, que é executado quando/se o promise atinge um estado de “pronto”. O Promise é criado por um Defirred Object. Primeiro você cria um Defirred Object e depois retorna deferred.promise(), que lhe dá o objeto Promise. O Deferred é usado para atualizar o status da ação assíncrona. Por exemplo, quando a ação for concluída, você chamaria deferred.resolve(). Quando isso é chamado, o promise vai executar todos os callbacks que foram registrados nela através dos métodos done, then e always.

Vejamos alguns exemplos para comparar os callbacks tradicionais usando promises. Eles foram retirados do Parse Blog, pois fazem um trabalho bastante decente para demonstrar a utilidade do uso de promises:

[javascript]

// Traditional example using nested ‘success’ callbacks
Parse.User.logIn("user", "pass", {
success: function(user) {
query.find({
success: function(results) {
results[0].save({ key: value }, {
success: function(result) {
// the object was saved.
}
});
}
});
}
});

// Another traditional example using plain callbacks (without ‘success’)
Parse.User.logIn("user", "pass", function(user) {
query.find(function(results) {
results[0].save({ key: value }, function(result) {
// the object was saved.
});
});
});

[/javascript]

Como você pode ver, em ambos os casos você acaba nidificando mais e mais em cada ação que você executa. Aqui está o que aconteceria se todos os três métodos utilizados no exemplo acima retornassem promises.

[javascript]

// Promises example using ‘then’
Parse.User.logIn("user", "pass").then(function(user) {
return query.find();
}).then(function(results) {
return results[0].save({ key: value });
}).then(function(result) {
// the object was saved.
});

[/javascript]

Como você pode ver, não importa quantas ações realizemos, a identação só vai para um nível profundo. Na forma como está escrito, lê-se muito facilmente: “login, em seguida encontrar, depois salvar, então … tudo o que fazemos quando ele é salvo.”

Para fazer o encadeamento como é feito acima, temos que usar then, porque then retorna um novo promise que é resolvido ou quando a função callback retorna um não-promise ou a promessa de que a função callback retorne seja resolvida.

Para saber mais sobre promises, você deve verificar a biblioteca Q e sua documentação. jQuery também tem uma implementação de promises, mas, como um artigo de Domenic Denicola observou, é um pouco quebrada. Eu ainda tendo a usar a implementação do jQuery, porque eu não preciso de uma biblioteca adicional e, até agora, ele atende às minhas necessidades.

Events e event hub

Já falei sobre o uso de arquiteturas baseadas em events, mas vou tocar nesse assunto um pouco mais aqui. Melhor, eu vou dar exemplos mais concretos. Usar a arquitetura baseada em events é semelhante à forma tradicional de callback de fazer as coisas, a não ser que você registre o callback de antemão e que persista no uso quando um event é disparado novamente mais tarde. Nós vamos usar o sistema de event do Backbone, porque é parecido com o que eu estou tentando usar no meu aplicativo. Se você não estiver familiarizado com Backbone, sugiro dar uma olhada na minha série screencast sobre Backbone, mas cuidado que as versões mais recentes do Backbone tornam isso um pouco obsoleto. Não se preocupe, eu vou montar algo para lhe mostrar todas as mudanças após a versão 1.0 ser lançada.

O exemplo a seguir é parte de um aplicativo que inicia e para servidores que executam no backend. O aplicativo do cliente faz chamadas para o backend para iniciar um servidor.

[javascript]

// The view will do something when a model finishes doing something asynchronous
ServerView = Backbone.View.extend({
initialize: function() {
this.model.on(‘started’, this.serverStarted, this);
},

serverStarted: function() {
// change something about view to indicate to users that
// the server is running
},

startServer: function() {
this.model.start();
},

});

Server = Backbone.Model.extend({
initialize: function() {
// React when the application lets us know a server was started
AppEvents.on(‘server:started’, this.started, this);
},

start: function() {
// Using a utility class, make a call to the back end to start the server.
// When a success message comes back from the back end, the utility will
// trigger an application-wide event to inform the entire system that a
// server has been started.
communicator.startServer(this);
},

started: function(serverID) {
if (serverID == this.get(‘id’)) {
// trigger the ‘started’ event
this.trigger(‘started’, this);
}
},

});

server = new Server();
view = new ServerView({model:server});

[/javascript]

Há muito mais para esse exemplo, mesmo que ele essencialmente só faça uma coisa. Uma coisa que eu não mencionei no código é como método startServer da view é chamado. Vamos supor que ele seja feito através de interação com o usuário, como clicar em um botão de “start server”.

Como você pode ver, nas funções initialize de cada uma das “classes” acima, registramos nossos manipuladores de eventos. Isso só acontece uma vez, por isso mesmo que comecemos (e paremos – apesar de eu não mostrar o código para parar) um servidor várias vezes, os manipuladores já existem e estão prontos para lidar com qualquer event.

A comparação

Você vê as diferenças impressionantes que os events realizaram?

  1. As funções start na view e no modelo são muito pequenas e só fazem uma coisa: iniciar o servidor (de acordo com suas respectivas abstrações).
  2. Agora, todo o sistema é capaz de saber sobre o início do servidor. Nada precisa ter conhecimento sobre qualquer modelo individual dos servidores, mas pode reagir quando um deles inicia.

Os exemplos de código para promises praticamente mostraram alguma programação processual. Isso tudo é muito bom, mas e a programação orientada a objeto? Métodos dos objetos precisam ser sucintos, e se um único método está lidando com tudo o que é mostrado nesse exemplo, pode ser uma boa ideia refatorar.

Eu também gosto mais da arquitetura baseada em event nesse caso, porque na minha aplicação real eu estou usando WebSockets para dizer ao backend para iniciar o servidor. WebSockets já são baseados em events, por isso parece fazer sentido usar events para lidar com esse tipo de coisa.

Finalmente, nesse exemplo, temos várias camadas de abstração (além de mais um na minha aplicação real), então, para a maior parte, eu estou apenas passando o promise em todo o caminho de volta, e ninguém a está usando até que ela chegue à view, em cujo caso o promise seria utilizado para fazer mais do que iniciar o servidor, de modo que ele não deveria estar no método startServer.

Com toda a justiça, você pode enviar uma função callback com WebSockets (pelo menos com Socket.IO; eu não tenho certeza sobre os próprios WebSockets) e utilizá-la para resolver o promise, bem como alertar o resto do sistema. Em outras palavras, você pode usar uma combinação de promises e events, mas isso torna difícil decidir qual é a melhor prática em cada situação individual. No entanto, à media que minha aplicação cresce, eu posso acabar precisando fazer exatamente isso. O tempo dirá.

Conclusão

A razão pela qual eu escrevi este artigo é porque recentemente passei muito tempo discutindo comigo mesmo sobre essa questão. Como devem ser implementadas as ações assíncronas na minha aplicação? Eu pensei muito nisso, e mesmo quando eu escrevi sobre isso, eu pensei  ainda mais. Ambas são grandes técnicas e merecem ser olhadas. No final, este artigo é mais para você pensar sobre suas decisões de design assíncrono do que para defender uma metodologia sobre a outra.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.joezimjs.com/javascript/javascript-asynchronous-architectures-events-vs-promises/