Javascript Nao E Bagunca

Javascript não é bagunça

Eu realmente gosto de Javascript. Quando vi o nascimento da plataforma node.js (node.js não é linguagem, ok?), fiquei empolgado com a idéia de poder fazer um scriptizinho.js (é ótimo ouvir isso de um gerente, não?) no lado back da força. Os primeiros exemplos de implementação sobre a plataforma não se preocupavam com a qualidade do código sugeridas pelos Jedis (Uncle Bob, Martin Fowler, Kent Beck, Booch, Gangue de Quatro* e etc.). Confesso que isso me deixava desanimado e me questionei se eu deveria investir o meu tempo neste novo mundo. Acertei! Hoje a coisa toda evoluiu e podemos afirmar que Javascript não é bagunça (ou talvez nunca tenha sido).

“TÁ” FECHADO, MAS “TÁ” ABERTO?

Quero escrever sobre um assunto que gosto muito: SOLID. Começo pela letra O do acrônimo por acreditar ser o de mais fácil implementação, inclusive em códigos existentes. Em linhas gerais o que Bertrand Meyer diz sobre o princípio do “aberto-fechado” é o seguinte:

“Uma classe só deve ser modificada quando erros forem encontrados. A classe pode ser estendida, mas nunca modificada.”

É difícil entender o valor do “aberto-fechado”. Talvez seja necessário escrever muitos códigos ruins para enxergar o real benefício deste princípio mas admito, minha vida ficou muito melhor depois que eu passei a escrever código limpo.

BUSCAFRETE — ”BUSCAPÉ” DOS FRETES

Acredito que exemplos de códigos sobre projetos factíveis são mais fáceis de entender e por isso, vamos imaginar o seguinte produto.

“Nós do grupo Eike Zords queremos construir uma solução inovadora para o setor logístico. Esse produto deverá ser capaz de retornar o menor custo de frete a partir de algumas variáveis: origem, destino e peso da mercadoria. Segundo nosso CEO, este produto já vale 1 bilhão de reais. “

Com o escopo do nosso MVP definido, chegou a hora de codar.

function CalculadoraFrete(origem,destino,peso){
 var _distancia = googleMaps.getDistance(origem,destino);
 var _valorFrete=[];
 var _repositorio = new RepositorioTaxasFrete();
 function buscaMenorFrete(){
  if(peso <= 30){
   if(_distancia < 50){
    _valorFrete.push(_repositorio.taxaMotoFreteMunicipal());
   }else{
    _valorFrete.push(_repositorio.taxaCorreios());
   }
  }
  else if(peso>30 && peso<800){  
   if(_distancia < 140){
    _valorFrete.push(_repositorio.taxaCarro());
   }else{
    _valorFrete.push(_repositorio.taxaCaminhao());
   }
  }
  else {
   if(_distancia < 300){
    _valorFrete.push(_repositorio.taxaCaminhao());
   }else{
    _valorFrete.push(_repositorio.taxaCarreta());
   }
  }
  _valorFrete.sort();
  return _valorFrete[0];
 }
}

O código acima apresenta diversos problemas mas quero comentar as deficiências sobre a ótica do “open-closed principle”.

Evolução dolorosa: A classe sempre será alterada quando funcionalidades forem adicionadas no produto. É o tipo de código que em pouco tempo ninguém mais quer colocar a mão. Legibilidade: As linhas acima são difíceis de ler, será complicado criar uma linguagem ubíqua com o dono do produto. “Véio, põe gordura pra mexer nessa bagaça” será a única técnica de estimativa adequada para tarefas que envolvam mexer no código acima.

Vamos melhorar a situação desse código? Começar pelos testes é uma técnica poderosa para se encontrar bons designs de objetos.

var assert = require(assert); // npm install -g assert
describe(CalculadoraFrete, function(){
   it(O valor do frete tem que ser R$ 30,00 para uma distância de 15 km no mesmo município, para uma mercadoria que pesa até 10kg, function(){

   var _CEPorigem = 01314000;
   var _CEPdestino = 01316000;
   var _pesoDaMercadoria = 10;
   var _valorDofreteEsperado = 30;
   var _resultado =    CalculadoraFrete.buscaMenorFrete(_CEPorigem,_CEPdestino,_pesoDaMercadoria);
   assert.equal(_esperado,_resultado);
   })
})

Para manter o foco sobre o assunto “open-closed” não vou copiar os códigos de testes que fiz. Eles realmente me ajudaram a evoluir o modelo.

Decidi criar uma especialização para cada tipo de frete. Deixo a responsabilidade de aceitar (ou não) fazer o cálculo do frete para algum especialista no assunto.

function TipoMotoFrete(){
  this.distanciaMaximaAtendida = 50;
  this.pesoMaximoAtendido = 30;
  this.podeAtenderEsseFrete = function(){
    return this.distancia < this.distanciaMaximaAtendida &&
           this.peso < this.pesoMaximoAtendido;
  }
  this.obterTaxaPorKM = function() {
   return 12.76;
  };
  this.obterTaxaPorKG = function() {
   return .70;
  };
}

Abaixo mais um tipo de frete

function TipoCorreioFrete(){
  this.pesoMaximoAtendido = 30;
  this.podeAtenderEsseFrete = function(){
    return this.peso < this.pesoMaximoAtendido;
  }
  this.obterTaxaPorKM = function() {
   return 10.97;
  };
  this.obterTaxaPorKG = function() {
   return .65;
  };
}

A distância não é importante para os “Correios” pois eles entregam em qualquer lugar do mundo! teoricamente.

Vou omitir a criação de todos os tipos de frete necessários para que otimizar o texto.

//Essa é a abstração para tipos de frete
function FornecedorFrete(distancia,peso){
 if (this.constructor === FornecedorFrete) { 
  throw new Error(Aqui é abstrato meu querido. Não vai estar
      podendo estar sendo construído. Nós da OO center
      agradecemos a compreensão.);
 };
 this.podeAtenderEsseFrete = function() {
  return this;
 };
 this.obterTaxaPorKG = function() {
  return this;
 };
 this.obterTaxaPorKM = function() {
  return this;
 };
 this.distancia = distancia;
 this.peso = peso;
 this.calculaValorFrete = function(){
   var _custoPeso = this.obterTaxaPorKG() * this.peso;
   var _custoDistancia = this.obterTaxaPorKM() * this.distancia;
   return  _custoPeso+_custoDistancia;
 };
};

Chegou o momento de colocar gerência na coisa toda. Vamos implementar um cara que seja responsável por dividir o trabalho entre todos os fornecedores de frete.

function CalculadoraFrete(distancia,peso){
 var _fornecedoresFrete = [];
 var _fretesEncontrados = [];
 _fornecedoresFrete.push(new TipoMotoFrete(distancia,peso));
 _fornecedoresFrete.push(new TipoCorreioFrete(peso));
 function obterMenorFrete(){
  for(var i=0;i<_fornecedoresFrete.length;i++){ 
   var _fornecedorFrete = _fornecedoresFrete[i];
   if(_fornecedorFrete.podeAtenderEsseFrete()){
    var _valorFrete = _fornecedorFrete.calculaValorFrete();
    _fretesEncontrados.push(_valorFrete);
   };
  };
  _fretesEncontrados.sort();
  return _fretesEncontrados[0];
 };
}

Terminado! Agora estamos preparados para receber diferentes tipos de fornecedores de frete e cada um deles decide se calcula o valor.

Pronto? Será que a “FornecedorFrete” não está com muita responsabilidade? Concordam que a forma de calcular o frete presente no método “calculaValorFrete” será alterada? Eu acredito que sim! Porém, quero fazer essa melhoria no momento que conversarmos sobre “Single responsibility principle” ou “cada macaco no seu galho”.

Compartilhar para transformar

Uma vez me disseram que é importante devolver ao mundo parte do que o mundo te deu e por isso resolvi compartilhar meu conhecimento sobre desenvolvimento de software.

Livro - Aplicações web real-time com Node.js

Estudar sempre é bom, nós desenvolvedores vivemos constantemente estudando novas linguagens, conceitos e boas práticas de código.

Hoje apresentarei a vocês o mais recente livro brasileiro sobre Node.js. E para estimular os seus estudos no final deste artigo há um cupom de descontos de cortesia da editora Casa do Código.

Sobre o livro

Livro: Aplicações web real-time com Node.js

O livro aborda de um forma prática, os conceitos por trás do Node.js. Nele você aprenderá sobre os recursos nativos e boas práticas de desenvolvimento com Node.js. No decorrer dos capítulos você também criará uma aplicação do zero, utilizando os principais frameworks da plataforma.

A aplicação criada no livro é uma agenda de contatos que permite conversar com os demais contatos onlines via web-chat que roda em tempo-real. Os frameworks utilizados são: Express, Socket.IO, Mongoose, Node-Redis, Mocha, SuperTest, Clusters, Forever e EJS. Um dos destaques do livro esta no capítulo final, em que é apresentado diversas dicas para preparar sua aplicação para um ambiente de produção. Até é abordado como integrar Node.js com Nginx aplicando proxy para o Node e deixando o Nginx servir arquivos estáticos.

O livro é recomendado para todos que tenham conhecimentos básicos de Javascript e domine os conceitos básicos de arquitetura cliente-servidor.

Nodejs e MongoDB - Introdução ao Mongoose

Mongoose é uma biblioteca do Nodejs que proporciona uma solução baseada em esquemas para modelar os dados da sua aplicação. Ele possui sistema de conversão de tipos, validação, criação de consultas e hooks para lógica de negócios.

Mongoose fornece um mapeamento de objetos do MongoDB similar ao ORM (Object Relational Mapping), ou ODM (Object Data Mapping) no caso do Mongoose. Isso significa que o Mongoose traduz os dados do banco de dados para objetos JavaScript para que possam ser utilizados por sua aplicação.

Obs: Este artigo vai assumir que você tem o Nodejs e o MongoDB instalados. Você precisa estar com o MongoDB rodando em seu computador para poder fazer os seguintes exemplos.

Instalando o pacote Mongoose

Utilizando o [NPM][] via linha de comando é muito simples fazer a instalação dos pacotes do Nodejs. Para fazer a instalação do Mongoose, execute a seguinte linha de comando que o NPM cuidará de instalar a versão estável mais recente do pacote requerido:

npm install Mongoose

Conectando com o MongoDB

Neste artigo vamos conectar o Mongoose com o banco de dados de testes que o MongoDB define quando você o inicializa pelo console e vamos garantir que qualquer erro de conexão seja impresso no console.

var Mongoose = require('Mongoose');

var db = Mongoose.connection;

db.on('error', console.error);
db.once('open', function() {
  console.log('Conectado ao MongoDB.')
  // Vamos adicionar nossos Esquemas, Modelos e consultas aqui
});

Mongoose.connect('mongodb://localhost/test');

Ao executar nossa aplicação de exemplo podemos observar no console do MongoDB que foram abertas 5 conexões simultâneas. Isto ocorre porque o Mongoose usa um conjunto de 5 conexões simultâneas por padrão que são compartilhadas por toda sua aplicação. Para melhorar o desempenho das nossas aplicações vamos deixá-las abertas, porém você pode alterar o comportamento padrão adicionando parâmetros opcionais ao Mongoose.connect() - o parâmetro poolSize define a quantidade de conexões simultâneas. Veja abaixo a saída do meu MongoDB exibindo que foram abertas 5 conexões simultâneas:

Sat Aug 31 11:12:21.827 [initandlisten] connection accepted from 127.0.0.1:64413 #2 (1 connection now open)
Sat Aug 31 11:12:21.830 [initandlisten] connection accepted from 127.0.0.1:64414 #3 (2 connections now open)
Sat Aug 31 11:12:21.831 [initandlisten] connection accepted from 127.0.0.1:64415 #4 (3 connections now open)
Sat Aug 31 11:12:21.831 [initandlisten] connection accepted from 127.0.0.1:64416 #5 (4 connections now open)
Sat Aug 31 11:12:21.832 [initandlisten] connection accepted from 127.0.0.1:64417 #6 (5 connections now open)

Esquemas e Modelos

Para começar vamos precisar de um esquema e um modelo para que possamos trabalhar com os dados que serão persistidos em nosso banco de dados MongoDB. Esquemas definem a estrutura dos documentos dentro de uma coleção e modelos são usados para criar instâncias de dados que serão armazenados em documentos. Nós vamos fazer um banco de dados de filmes para acompanhar os filmes que tem cenas extras após os créditos finais (também chamado de ‘credit cookies’). O código abaixo mostra nosso esquema movieSchema e nosso modelo Movie criado.

var movieSchema = new Mongoose.Schema({
  title: { type: String },
  rating: String,
  releaseYear: Number,
  hasCreditCookie: Boolean
});

var Movie = Mongoose.model('Movie', movieSchema);

A última linha deste código compila o modelo Movie utilizando o esquema movieSchema como estrutura. O Mongoose também cria uma coleção no MongoDB chamada Movies para estes documentos.

Você pode notar que o modelo Movie está em letra maiúscula, isto porque quando um modelo é compilado é retornado uma função construtora que será usada para criar as instâncias do modelo. As instâncias criadas pelo construtor do modelo são documentos que serão persistidos pelo MongoDB ao utilizar a função save.

Criar, Recuperar, Atualizar e Deletar (CRUD)

Criar um novo documento Movie é fácil agora que já definimos o modelo, basta instancializar o modelo Movie e salvar esta instância na base. Atualizar é igualmente fácil, faça suas modificações e então chame a função save do seu documento.

var thor = new Movie({
  title: 'Thor',
  rating: 'PG-13',
  releaseYear: '2011',  // Note o uso de String ao inves de Number
  hasCreditCookie: true
});

thor.save(function(err, thor) {
  if (err) return console.error(err);
  console.dir(thor);
});

Observe que utilizamos uma String ao invés de um Número no campo releaseYear e o Mongoose vai se encarregar de converter o dado para tipo especificado no esquema.

Quando adicionamos estes dois códigos dados à nossa aplicação e executamos vemos que a função save vai fornecer um documento recém criado, observado pelo que foi impresso no console de nossa aplicação.

{ __v: 0,
  title: 'Thor',
  rating: 'PG-13',
  releaseYear: 2011,
  hasCreditCookie: true,
  _id: 5222012cb65eddf003000001 }

Coleções no MongoDB possuem esquemas flexíveis, isto quer dizer que coleções não impõem a estrutura dos documentos. Na prática isto significa que documentos da mesma coleção não precisam ter o mesmo conjunto de campos ou estrutura, e campos comuns em documentos de uma coleção podem carregar diferentes tipos de dados. Como foi visto em nosso exemplo, utilizando o Mongoose para mapear nossa base, ele padroniza os documentos de um mesmo esquema a fim garantir que instâncias do modelo que compilou aquele esquema sempre tenham o mesmo tipo de dados nos atributos especificados pelo esquema.

MongoDB também possui uma estrutura de dados chamada índice que permite você localizar rápidamente documentos baseado nos valores armazenados em certos campos específicos. Fundamentalmente, índices no MongoDB é similar à índices em outros sistemas de banco de dados. Em nosso exemplo o índice do modelo criado é _id.

Recuperando um documento da base

Diferentes maneiras podem ser utilizadas para recuperar um documento existente na base de dados. Você pode buscar documentos baseado em qualquer propriedade do esquema e você pode buscar qualquer quantidade de documentos. Utilize findOne para limitar os resultados a um único documento.

// Buscando um unico filme pelo nome
Movie.findOne({ title: 'Thor' }, function(err, thor) {
  if (err) return console.error(err);
  console.dir(thor);
});

// Buscando todos os filmes
Movie.find(function(err, movies) {
  if (err) return console.error(err);
  console.dir(movies);
});

// Buscando todos os filmes que possuem 'credit cookie'.
Movie.find({ hasCreditCookie: true }, function(err, movies) {
  if (err) return console.error(err);
  console.dir(movies);
});

Mongoose também permite você criar funções auxiliares estáticas para buscar seus dados. Para isso você deve atribuir sua função estática ao esquema antes de ser feita compilação para o modelo.

movieSchema.statics.findAllWithCreditCookies = function(callback) {
  return this.find({ hasCreditCookie: true }, callback);
};

var Movie = Mongoose.model('Movie', movieSchema);

// Utilizadno a funcao auxiliar estatica do modelo 'Movie' compilado
Movie.findAllWithCreditCookies(function(err, movies) {
  if (err) return console.error(err);
  console.dir(movies);
});

Isso é tudo que precisa fazer para manipular os dados em MongoDB. Mongoose faz esta tarefa muito simples então você pode desenvolver seus serviços rapidamente. Usando isso junto com suas habilidades com Express, você pode desenvolver um aplicativo web muito bom e funcional.

Abaixo está um código utilizando os conceitos aqui apresentados que implementa a função auxiliar estática, para buscar todos filmes com ‘credit cookies’, salva 3 filmes do Thor com ‘credit cookies’ e em seguida cria um Timeout para buscar os filmes utilizando a função auxiliar 1 segundo depois - Isto é feito pois como estamos trabalhando com IO não bloqueante o Node não aguarda o tempo de salvar os filmes antes de prosseguir, por isso damos um tempo para se certificar que eles foram salvos antes de buscá-los.

var Mongoose = require('Mongoose');

var db = Mongoose.connection;

db.on('error', console.error);
db.once('open', function() {
  // Create your schemas and models here.
  console.log('Conectado.')
  
  
  
  var movieSchema = new Mongoose.Schema({
    title: { type: String },
    rating: String,
    releaseYear: Number,
    hasCreditCookie: Boolean
  });
  
  
  movieSchema.statics.findAllWithCreditCookies = function(callback) {
    return this.find({ hasCreditCookie: true }, callback);
  };

  var Movie = Mongoose.model('Movie', movieSchema);

  
  for (var i =1; i<=3; i++){
    var thor = new Movie({
      title: 'Thor ' + i,
      rating: 'PG-13',
      releaseYear: '2011',  // Note o uso de String ao inves de Number
      hasCreditCookie: true
    });

    thor.save(function(err, thor) {
      if (err) return console.error(err);
      console.log('Salvo: ')
      console.dir(thor);
    });
  }
  
  setTimeout(function(){
    // Utilizadno a funcao auxiliar estatica do modelo 'Movie' compilado
    Movie.findAllWithCreditCookies(function(err, movies) {
      if (err) return console.error(err);
      console.log('Buscado: ')
      console.dir(movies);
    });
  }, 1000);
});

Mongoose.connect('mongodb://localhost/test');

Mineração de dados e as funções map, reduce e filter

Este artigo vai mostrar como é simples fazer uma mineração de dados na Internet com o Nodejs e demonstrando como utilizar funções muito úteis do JavaScript: map, reduce e filter.

Bancos de dados não-relacionais (NoSQL), como o CouchDB e o mongoDB, utilizam o MapReduce, que é uma combinação das funções map e reduce apresentadas aqui, para efetuarem suas consultas na base de dados. Eles implementam funções de maneira muito similares às do JavaScript e este é o melhor motivo para compreender o funcionamento desta operação.

O texto também serve de base para quem deseja fazer um web crawler, também chamado de web spider, que são programas que acessam páginas da web e extraem informações relevantes de seu conteúdo. Os motores de busca, como a Google e o Bing, utilizam esta técnica para manterem suas bases de dados atualizadas. Os crawlers também são utilizados para varrer a Internet a fim de minerar endereços de email e dados pessoais.

Buscando o conteúdo na Web

A fonte de dados utilizada será o Feed RSS do HackerNews, como o RSS está em XML podemos tratá-lo da mesma forma que um documento HTML.

Para dar início ao nosso projeto temos que criar nosso aplicativo e fazer com que ele busque todo o Feed para que seja possível fazer a extração dos dados. Para isso será utilizado a função get do módulo https. Observe como o aplicativo exemplo app.js ficou.

// app.js

var https = require('https');

function getCallback(response){
  var body = '';
  console.log("Temos uma resposta: " + response.statusCode);
  response.on('data', function (chunk) {
    body += chunk;
  });
  response.on('end', function(){
    console.log("Corpo da mensagem: " + body.slice(0,200) + '...');
  });
}

https.get('https://news.ycombinator.com/bigrss', getCallback)
  .on('error', function(e){
    console.log("Ocorreu um erro: " + e.message);
  });

Vamos acompanhar o que é feito neste código passo a passo. Primeiro importamos o módulo https que buscará o endereço da web que queremos. Depois definimos a função getCallback que, como o nome sugere, será a função callback do nosso https.get(), ela recebe o objeto response como argumento e é adicionado um event listner para o evento data, que concatena a resposta na variável body, e um event listner para o evento end, que chama a função console.log() quando a resposta de nossa requisição está completa. Por último executamos o método https.get() passando como parâmetro o endereço do Feed que vamos buscar e a função callback que tratará a resposta, adicionamos também um event listner para o evento error que nos indicará se ocorreu algum erro durante nossa requisição.

É interessante ressaltar que o Node trabalha com eventos, então cada pacote recebido como resposta é um evento que deve ser tratado no nosso objeto response, por isso temos que concatenar toda a resposta a fim de trabalhar com o corpo completo da página requisitada.

Minerando os dados

Agora que temos o corpo da página temos que fazer a extração dos dados que queremos e do jeito que queremos. Neste exemplo vamos demonstrar as habilidades das funções map, reduce e filter desenvolvendo um contador de palavras dos títulos dos posts. Ele contará o número de ocorrências de cada palavrá e retornará uma lista de túplas [palavra, nro_ocorrências] das palavras que ocorreram mais de 10 vezes nos títulos.

Como foi dito, a base de dados que vamos minerar está em RSS então ela pode ser tratada da mesma forma que uma página HTML. Para isso podemos utilizar o jQuery para extrair a informação formatada.

Para utilizar as funções do núcleo do jQuery no servidor podemos tanto utilizar a própria biblioteca jQuery, desenvolvida para o navegador, como utilizar a biblioteca Cheerio, que é uma implementação rápida, flexível e limpa do core do jQuery designada específicamente para o servidor. Matthew Mueller diz que seus benchmarks sugerem que Cheerio seria 8 vezes mais rápida que a implementação do jQuery para o navegador.

Antes de utilizá-la, vamos fazer a instalação do pacote do Cheerio para em nosso aplicativo. Para isso basta instalá-lo utilizando a NPM via linha de comando:

npm install cheerio

Vamos, então, importar o módulo para nosso aplicativo e criar uma função para minerar os dados da página requisitada. Vamos chamar nossa função de minerarDados() e passar para ela o corpo da resposta recebida pela função getCallback(). O código do nosso exemplo ficaria assim:

var https = require('https'),
    cheerio = require('cheerio');

function minerarDados(body){
  var $ = cheerio.load(body);
  var titles = $('title');
  console.log(titles.slice(0,2));
}

function getCallback(response){
  var body = '';
  console.log("Got response: " + response.statusCode);
  response.on('data', function (chunk) {
    body += chunk;
  });
  response.on('end', function(){
    minerarDados(body);
  });
}

https.get('https://news.ycombinator.com/bigrss', getCallback)
  .on('error', function(e){
    console.log("Ocorreu um erro: " + e.message);
  });

Agora nossa função minerarDados() conseguiu extrair os objetos do tipo tag de nome title e imprime dois deles no console. Ok, ela extrai todos os títulos do Feed RSS, porém temos um problema, $('title') retorna uma lista de nós (NodeList). Vamos então aplicar o MapReduce para extrair a informação que desejamos e por último filtrar os resultados indesejados.

Para converter-mos nossa lista de nós em um Array contendo apenas o conteúdo de texto dos nomes vamos primeiro transformar nossa NodeList em um Array de objetos utilizando a função Array.prototype.slice.call() e neste array de objetos vamos aplicar a função map() do JavaScript, o código ficaria assim.

var titles = Array.prototype.slice.call($('title')).map(
  function(node, index, context) {
    return $(node).text();
});

array.map(callback[, thisArg])

A função map() recebe dois parâmetros mas normalmente apenas o primeiro é especificado. O primeiro parâmetro é uma função de callback que será chamada sobre todos os elementos no Array. O segundo parâmetro é utilizado para especificar o valor para o objeto this durante a execução da função. O mais importante são os parâmetros passados para a função de callback, eles são: o elemento do Array em sí, o índice do Array, e todo o Array (contexto). A assinatura da função de callback se parece com essa:

var callback = function(elemento, indice, contexto) { /* omitido */ }

Porém também podemos utilizar a função map() do jQuery implementada pelo Cheerio, mas para isso é preciso ter atenção pois a assinatura da função callback do jQuery é ligeiramente diferente, em seus parâmetros primeiro vem o índice do elemento seguido do elemento em sí. Sua assinatura é assim:

var callback = function(indice, elemento) { /* omitido */ }

Para implementar utilizando a função map() do jQuery é preciso fazer ligeiras modificações, o código deverá se parecer com isto:

var titles = $('title').map(
  function(index, node) {
    return $(node).text();
});

Eliminando pontuações desnecessárias

Agora que já temos um Array de strings contento o texto de todos os títulos da nossa base de dados que estamos explorando. Você já deve ter notado que os títulos não contém apenas letras, eles também contém números e pontuações. Estes são desnecessários em nosso contador de palavras, então vamos tratar de removê-los. Isto pode ser feito nesta mesma chamada map() utilizando expressão regular.

var words = $('title')
  .map(function(index, node) {
    return $(node).text().toLowerCase().match(/([a-z]+)/g);
  });

Se você já utilizou RegEx (expressões regulares) no JavaScript você sabe que isso gera um efeito colateral: Esta função retorna Arrays com todas as palavras que passaram na RegEx individualmente. Contudo esse é um efeito colateral útil para nós pois só precisamos das palavras, no entanto temos mais um problema: o Array words retornado é de duas dimensões, isso que significa que temos que achatá-lo para apenas uma dimensão. Este é um bom momento para fazer uso da função reduce() do JavaScript.

var words = $('title')
  .map(function(index, node) {
    return $(node).text().toLowerCase().match(/([a-z]+)/g);
  })
  .reduce(function(last, now){
    return last.concat(now)
  }, []);

array.reduce(callback[, initialValue])

Assim como map, reduce recebe dois argumentos. O primeiro é novamente a função de callback, que será chamada para cada elemento no Array. O segundo parâmetro é o initialValue que será de fácil entendimento quando você ver a assinatura da função de callback:

var callback = function(valorAnterior, valorAtual, indice, contexto) { /* omitted */ }

Como você pôde ver, o primeiro parâmetro passado para a função de callback é o valor anterior. Se você precisar somar um Array de números, isso não será problema. Mas em nosso caso nós precisamos retornar um Array, então nós especificamos um valor inicial para o valorAnterior, neste caso um Array vazio [] onde serão adicionados os elementos de nossa antiga Array bi-dimensional.

Contando as palavras

Para esta tarefa podemos utilziar reduce novamente, mas antes de fazê-lo, vou mostrar como o resultado vai se parecer:

[['the', 'on', 'news', 'hacker', ...], [50, 66, 20, 19, ...]]

Isso será uma Array de duas dimensões novamente. São dois Arrays dentro de um Array, e o índice da palavra no primeiro Array corresponderá com o índice do número de vezes que ela ocorre nos títulos no segundo Array. Para fazer isso vamos utilizar reduce e vamos especificar uma Array 2d vazia como valor inicial: [[], []].

var scores = $('title')
  .map(function(index, node) {
    return $(node).text().toLowerCase().match(/([a-z]+)/g);
  })
  .reduce(function(last, now){
    return last.concat(now)
  }, [])
  .reduce(function(last, now){
    var index = last[0].indexOf(now);
    if (index === -1) {
      last[0].push(now);
      last[1].push(1);
    } else {
      last[1][index] += 1;
    }
    return last;
  }, [[], []]);

Comprimindo os Arrays

Estamos próximos de concluirmos nossa coleção de dados. Tudo que devemos fazer agora é combinar as duas Arrays em uma. O formato final do Array será este:

[['the', 50], ['on', 66], ['news', 20], ['hacker', 19], ...]

O JavaScript não implementa a função zip() nativamente, nós poderíamos utilziar esta função do pacote Underscore mas como esta função é muito simples de ser implementada vamos implementar nós mesmos a mesclagem dos valores destas listas.

var zip = [];
scores[0].forEach(function(word, i) {
  zip.push([word, scores[1][i]])
});

Filtrando os dados

Lembrando que nossa aplicação só se interessa por palavras que ocorreram mais de 10 vezes, a fim de filtrar as palavras menos importantes da lista, então vamos implementar esta última funiconalidade. Para isto podemos utilizar a função nativa do JavaScript filter da seguinte maneira:

var filtered = zip.filter(function (element){
  return element[1] >= 10
});

array.filter(callback[, thisObject])

Os parâmetros da função filter são exatamente os mesmos da função map já apresentada, ela possui dois parâmetros porém normalmente só o primeiro é especificado. O primeiro parâmetro é uma função de callback que será chamada sobre todos os elementos no Array. O segundo parâmetro é utilizado para especificar o valor para o objeto this durante a execução da função. O importante é a função de callback, seus parâmetros são: o elemento do Array em sí, o índice do Array e todo o Array (contexto). A assinatura da função de callback se parece com essa:

var callback = function(elemento, indice, contexto) { /* omitido */ }

Esta função retorna verdadeiro ou falso para filtrar os elementos que continuam no Array e os elementos que são excluídos do Array. Quando fizemos element[1] >= 10 utilizamos o operador lógico de comparação que retorna verdadeiro para todos os elementos cujo valor do índice 1 seja maior ou igual a 10 e falso caso contrário - o índice 1 guarada a quantidade de vezes que a palavra ocorreu nos títulos.

Conclusão

Agora nosso exemplo está concluido e conseguímos fazer a mineração dos dados que queríamos dos Feeds do HackerNews. O código final ficou assim:

var https = require('https'),
    cheerio = require('cheerio');

function minerarDados(body){
  var $ = cheerio.load(body);
  var scores = $('title')
    .map(function(index, node) {
      return $(node).text().toLowerCase().match(/([a-z]+)/g);
    })
    .reduce(function(last, now){
      return last.concat(now)
    }, [])
    .reduce(function(last, now){
      var index = last[0].indexOf(now);
      if (index === -1) {
        last[0].push(now);
        last[1].push(1);
      } else {
        last[1][index] += 1;
      }
      return last;
    }, [[], []]);
  var zip = [];
  scores[0].forEach(function(word, i) {
    zip.push([word, scores[1][i]])
  });
  var filtered = zip.filter(function (element){
    return element[1] >= 10
  });
  console.log(filtered.slice(0,20));
}

function getCallback(response){
  var body = '';
  console.log("Got response: " + response.statusCode);
  response.on('data', function (chunk) {
    body += chunk;
  });
  response.on('end', function(){
    minerarDados(body);
  });
}

https.get('https://news.ycombinator.com/bigrss', getCallback)
  .on('error', function(e){
    console.log("Ocorreu um erro: " + e.message);
  });

O exemplo apresentado funcionou corretamente e conseguiu extrair a informação da base da Internet escolhida e imprimir os 20 primeiros elementos do nosso Array no console console.log(filtered.slice(0,20));.

O artigo teve como finalidade ensinar os leitores como é fácil fazer tal operação utilizando o Nodejs e as funções nativas do JavaScript, podendo ser utilizado como base para desenvolvimento de aplicações mais complexas como robôs de web crawling entre outros.

Como evitar o inferno de callbacks

Este artigo é um guia para se escrever programas assíncronos em JavaScript que expõe dicas para se evitar o ‘inferno de callbacks’.

O bom entendimento de callbacks é essencial para a programação orientada a eventos do Node, este é o nome dado a funções que serão executadas de modo assíncrono, ou posteriormente. Caso tenha dúvidas do que é uma função callback você pode ler o artigo explicando callbacks em Node.

O que é o ‘inferno de callbacks’

JavaScript assíncrono, ou JavaScript que usa callbacks é difícil de se entender intuitivamente. Muitos códigos acabam se parecendo como este:

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))
        }
      })
    })
  }
})

Você consegue ver quantas functions e }) têm neste código? Isto é carinhosamente conhecido como o inferno de callbacks.

Escrever códigos melhores não é difícil, você só precisa saber algumas coisas.

Nomeie suas funções

Aqui esta um confuso código em JavaScript que roda no navegador que utiliza browser-request para fazer uma requisição AJAX para o servidor.

var form = document.querySelector('form')
form.onsubmit = function(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function(err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

O código possui duas funções anônimas, observe como fica o código quando damos nomes a estas funções.

var form = document.querySelector('form')
form.onsubmit = function formSubmit(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function postResponse(err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

Como você pode ver nomear as funções é muito fácil e traz algumas coisas boas para seu código:

  • Facilita a leitura do código
  • Quando acontecem exceções você pega rastreamento da pilha (stacktraces) que referencia o nome atual da função ao invés de “anonymous”
  • Permite você manter seu código mais raso, ou não aninhado profundamente

Mantenha seu código raso

Aproveitando o exemplo apresentado, vamos um pouco mais longe e se livrar do nível triplo de aninhamento que há no código.

function formSubmit(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse(err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

document.querySelector('form').onsubmit = formSubmit

Códigos como este facilitam a leitura e a manutenção posterior.

Modularize seu código

Esta é a parte mais importante: Qualquer pessoa é capaz de criar módulos (biblioteca AKA). Citando Isaac Schlueter (do projeto do Node): “Escreva módulos pequenos onde cada um faça uma coisa, e monte-os em outros módulos que fazem coisas maiores. Você não entra no inferno de callback se você não ir para lá.”

Vamos pegar o exemplo apresentado e transformá-lo em um módulo dividindo-o em um par de arquivos. Aqui será apresentado um método simples que funciona tanto no navegador como no servidor.

Vamos criar um arquivo chamado formuploader.js que contém nossas duas funções apresentadas anteriormente.

function formSubmit(submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse(err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

exports.submit = formSubmit

A variável exports da última linha é um exemplo do sistema de módulos CommonJS. O bom deste sistema de módulos é que ele é muito simples - você só precisa definir o que deve ser compartilhado quando o módulo for importado, para isto é utilizado a variável local exports do módulo. Para compreender melhor como funciona a importação de módulos você pode ler o artigo que explica como funciona a função require do Node.js;

O Node tem um sistema simples de carregamento de módulos que utiliza o padrão CommonJS e de fato a maior parte das funções do núcleo do Node é implementada utilizando módulos escritos em JavaScript. Compreender profundamente o sistema de módulos do Node é crucial para implementação de códigos legíveis e de fácil manutenção, fundamental para a maior parte das aplicações. Para entender melhor este assunto acesse o artigo que aborda detalhadamente os módulos em Node.js.

Para usar módulos no padrão CommonJS no navegador, você pode utilizar a biblioteca browserify. Esta biblioteca basicamente permite você utilizar a função require para carregar módulos no seu programa.

Agora que temos o formuploader.js e ele foi carregado na página utilizando a tag HTML script, nós só precisamos importá-lo e usá-lo. Veja como ficou o código da nossa aplicação agora:

var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

Agora nossa aplicação exemplo apresentada só possui duas linhas e tem os seguintes benefícios:

  • Facilita o entendimento de novos desenvolvedores - Eles não precisam empacar tentando ler todas as funções formuploader para entender o que está acontecendo aqui
  • As funções do formuploader agora podem ser usadas em outros lugares sem a duplicação de código e pode facilmente ser compartilhaada no GitHub
  • O código em sí é agradável, simples e fácil de ler

Há muitos padrões de módulos para o navegador e para o servidor. Alguns deles são muito complicados. Os mostrados aqui são os considerados mais simples de se entender.

E o Promises?

Promises é o padrão mais abstrato para trabalhar com código assíncrono em JavaScript.

O escopo deste artigo é mostrar como escrever um código agradável em JavaScript. Se você utiliza uma biblioteca de terceiros que adiciona abstração para seu JavaScript, então certifique-se de estar disposto a forçar todos que contribuem para a sua biblioteca a também terem os mesmos pontos de vistas sobre o JavaScript que você.

Max Ogden expõe que utiliza callbacks em 90% do código assíncrono que escreve e quando as coisas se complicam ele utiliza algo como a biblioteca async, que é um módulo que fornece funções poderosas para trabalhar com código JavaScript assíncrono, ela foi projetada para utilizar como o Node.js mas também pode ser utilizada diretamente no navegador.

Leitura adicional