Atenção: a didática deste post foi desenvolvida para quem já tem algum conhecimento em Backbone. Se você estiver começando agora, do zero, este artigo não é para você.
***
Howdy, partners! Imagine que você esteja fazendo uma lista de compras no seu site com Backbone.js. Agora, para ilustrar melhor ainda, imagine que você tem algo mais ou menos assim:
Tecnicamente falando, podemos abstrair este simples widget em três camadas:
- O “#products-header”, onde aparece “1 produto adicionado:”;
- O “#products-list”, onde aparece o “Livro de Backbone.js (2)”;
- O “#products-footer”, onde aparece o botão “Adicionar produto”.
Então, com esse modelo de abstração, tenho três views, sendo, respectivamente:
- MyApp.Views.Produts.Header;
- MyApp.Views.Products.List;
- MyApp.Views.Products.Footer.
Agora, vamos analisar as nossas regras:
- Product é um modelo (em português brasileiro) ou model (em inglês);
- Products é uma coleção (em português brasileiro) ou collection (em inglês);
Portanto, Products possui vários Product, o que é facilmente exemplificado no mundo real como uma “Products” sendo uma “lista” de produtos (ou, Product). Products já vive e já é representado através da sua própria view (MyApp.Views.Products.List), ou seja, já sabemos como ele é; mas e um produto em si, como ele se parece? Pois é… Teríamos, por fim, uma quarta view invocada como MyApp.Views.Products.Item. Então, uma lista de produtos pode ter um ou mais produtos – e esse valor é representado através da view MyApp.Views.Products.Header através do “1 produto adicionado” – lembra? Os envolvidos e responsáveis por toda a mágica até agora são só e somente só Products – que “armazena” os produtos num formato de lista – e Product – que é a encarnação de um produto qualquer. Por fim, como que nos comunicaríamos com o cabeçalho (ou “header”, em inglês, se preferir) através da MyApp.Views.Products.List? Ora, quando um novo Product for adicionado à Products, o “Header” precisa saber incrementar o valor por ele comportado.
Introduzindo EventAggregators
Um Event Aggregator não é um mecanismo do Backbone ou exclusividade de alguma tecnologia específica, mas sim um serviço de intermediação entre “coisas”: ele ouve uma mensagem e a leva até algum lugar, onde, nesse lugar, o receptor fará algo baseado nessa mensagem. Em linhas curtas, você pode, por exemplo, usá-lo para intermediar eventos entre camadas. Mas, “camadas”? O que isso significa exatamente? Então, esse termo é conceitual, eu sei – para você entender melhor, me refiro à camadas como sendo classes, protótipos ou arquivos; entidades diferentes, por assim falar melhor. Na prática, List é o recipiente de Products – precisa explicar pro Header para somar 1 (um) ao valor da sua contagem de produtos (somente) quando um item for adicionado. Para isso, já vi soluções como essa:
/** * A view "concreta" da lista de produtos – eis a * encarnação da coleção. */ MyApp.Views.Products.List = new Backbone.View.extend({ /** * Aqui você tem os atributos e/ou métodos * da sua view que funcionam perfeitamente. */ collection: MyApp.Collections.Products, add: function () { var item = new MyApp.Models.Product({ id: 1, name: 'Livro Backbone.js' }); this.collection.add(item); var counter = new MyApp.Views.Products.Header; counter.refresh(this.collection.length); }); /** * A view "concreta" do cabeçalho, onde o contador * fica "hospedado". */ MyApp.Views.Products.Header = new Backbone.View.extend({ /** * Aqui você tem os atributos e/ou métodos * da sua view que funcionam perfeitamente. */ refresh: function (quantity) { this.$el.find('.counter').html(quantity); } });
E aí, o que você acha? Em primeiro lugar, se você copiar e colar este código em um JavaScript seu, não vai funcionar. Isso porque são códigos demonstrativos e estão incompletos, mas a demonstração é conceitual. Em segundo lugar, se o código estivesse completo e seguindo esse conceito, essa “técnica” funcionaria sim. Entretanto, está (muito) longe de estar correta e incentivada, porque não faz sentido algum você instanciar (veja o new) uma view para essa finalidade – na verdade, você pode instanciar quantas views você quiser, desde que seja para a sua real finalidade: renderização. Em terceiro lugar, (já) existe uma solução muitíssimo mais elegante para contornar essa situação. Aliás, usar a palavra “incorreto” é errado, porque soa como uma “gambiarra”. Veja:
/** * No escopo global, você deve injetar ao Backbone * o (seu) método personalizado de agregação de eventos, * como o a seguir. * * Lembrando que o objeto "Backbone" precisa estar também * disponível globalmente para que a injeção seja feita * com êxito. */ Backbone.EventAggregator = _.extend({}, Backbone.Events);
Depois nós podemos finalmente fazer com que um evento exploda quando um item – ou produto, como preferir – for adicionado à sua lista de produtos:
/** * A view "concreta" da lista de produtos – eis a * encarnação da coleção. */ MyApp.Views.Products.List = new Backbone.View.extend({ /** * No método a seguir, estamos à adicionar * um item à lista. * * Exatamente nesse momento é que temos que incrementar * o valor contido lá no cabeçalho – para isso, * dessa vez, usaremos o EventAggregator. */ add: function () { var item = new MyApp.Models.Product({ id: 1, name: 'Livro Backbone.js' }); this.collection.add(item); /** * O valor do primeiro parâmetro é o nome dado à ação * engatilhada. * * Note que esse nome nada tem a ver com o método * a ser executado quando este engatilhador for acionado. * * O valor do segundo parâmetro, por sua vez, é o dado * que você quer transportar de, no nosso caso, uma * view para a outra. */ Backbone.EventAggregator .trigger('refreshCounter', this.collection.length); } }); /** * A view "concreta" do cabeçalho, onde * o contador fica "hospedado". */ MyApp.Views.Products.Header = new Backbone.View.extend({ /** * Dessa vez, apresento-lhes o construtor: aqui, você * invocará um "listener", este que será responsável * por "fazer alguma coisa" quando "algo acontecer". */ initialize: function () { /** * O método em questão já não é mais o ".trigger()", * mas sim o ".on()". Traduzindo arbitrariamente: * "Agregador de eventos, quando 'refreshCounter' * for acionado, execute 'this.refresh' enviando * como parâmetro 'this'". * * Assim, após a execução de ".add()" da nossa * view "List", executaremos o "refreshCounter" que, * por sua vez, executará o método ".refresh()" da * view "Header". * * Para constar, o "this" em ".refresh" significa * que é um método * do objeto instanciado. A ausência de parênteses, * por outro lado, se traduz por uma trivial * referenciação ao método. */ Backbone.EventAggregator .on('refreshCounter', this.refresh, this); }, /** * O método mantém-se intacto, como você pode ver. */ refresh: function (quantity) { this.$el.find('.counter').html(quantity); } });
Além de termos menos código utilizando o EventAggregator, estamos utilizando de uma boa prática que só vai somar na escalabilidade e manutenção do nosso software.
Quero salientar ainda que existem serviços de eventos prontos e relativamente mais avançados, como o já falado Backbone.Wreqr – já depreciado – e o Backbone.Radio; portanto, fazendo jus ao título, apresentei a forma mais pura possível, sem injetar qualquer terceirização ao seu Backbone e naturalmente ao seu software.
Se você quiser uma leitura mais aprofundada sobre o serviço EventAggregator, recomendo esta leitura, desenvolvida pela Microsoft, e esta outra, de 2004, por Martin Flower. Por hoje é isso! Espero que gostem.