Reflexão em padrões de projeto para Node.js

Emanuel G de Souza
Feb 24, 2017 · 6 min read

Bem pessoal, estou aqui traduzindo mais um texto. Agora sobre Node.js, o JavaScript no servidor. O texto a seguir é uma reflexão sobre os padrões de projeto nessa plataforma. Espero que ele seja de muito proveito a você, desenvolvedor JS. Confira o texto original, Refleting on node.js design patterns, de Kalin Chernev, aqui mesmo no Medium.com.


Era uma vez, havia um designer …

Créditos: commitstrip.com

Não importa se a história é verdade ou não, trabalhar com JavaScript no servidor com Node.js no momento é uma experiência muito diferente em comparação ao trabalho com Python, Ruby ou PHP. Eu pessoalmente digo que a vantagem de se trabalhar com uma única linguagem em todos os lugares vem com o preço de ter que aprender diferentes padrões de design, a fim de usar a linguagem de forma eficaz.

Um destes conceitos fundamentais a se aprender é a programação assíncrona. Há muitos artigos na comunidade Node.js / JavaScript sobre programação assíncrona em termos do que são callbacks e como resolver problemas de fluxo de controle com uma bíblioteca, uma promise, async / wait etc. Há alguns que são úteis quando a questão geralmente é:

Como usar módulos no node de uma boa forma?

E embora seja importante aprender e seguir boas práticas de implementação, faltam artigos que respondam a um outra questão:

Como organizar meu código em módulos no node de uma boa forma?

E neste texto, estarei refletindo sobre alguns padrões que respondem a esta última.

Padrão de estilo Continuation-passing

Este é um padrão simples de entender, mas fundamental para se trabalhar com código assíncrono.

Resumo: em sua função Continuation-passing (CPS):

  • Use cb(null, data) ao invés de return para passar um resultado;
  • Use return cb(err) para passar um erro e fechar a execução de uma função;
  • Comunicar um único resultado da função.
// Synchronous
function add(a, b) {
return a + b;
}

console.log(add(2, 2));
// Asynchronous
function addAsync(a, b, cb) {
cb(a + b);
}

addAsync(2, 2, function add(result) {
console.log(result);
});

É isso aí. Honestamente, a primeira vez que vi este tipo de código, eu senti um abrir de olhos. Se você já sabia disso, parabéns a você e bom trabalho!

Só por uma questão de estética, é possível refatorar esta última função para:

addSync(2, 2, result => console.log(result));

Ou ainda:

addSync(2, 2, console.log);

O importante de se entender nesta abordagem é que funções podem ser passadas como parâmetros para outras funções. São funções que agem como declaração de retorno.

Em Node.js, por convenção, se diz que você, ao escrever a sua função CPS, você deve enviar o erro da função como primeiro argumento do callback.

Vamos fazer algo um pouco mais prático — definir uma função que pegue uma lista de arquivos. Se há os tais arquivos, ela os retorna. Se não, ela devolve um erro.

function readFiles (files, cb) {
if (files.length) {
cb(null, files)
} else {
cb('no files supplied')
}
}

Esta função será chamada da seguinte forma:

readFiles(process.argv.slice(2), (err, data) => {
if (err) return console.error(err)
console.log(data)
})

Eu recomendo altamente que você teste isso se não sabe como funciona. Se você é preguiçoso para abrir uma sessão do terminal, basta usar um Runkit e e copiar-colar isto no navegador.

Note que o return é usado no caso de um erro parar a execução da função. Este padrão é muito popular e vem a calhar para quase todos os casos em que o consumidor do seu módulo precisa fazer uma única coisa e obter um único resultado.

Padrão Observer com o EventEmitter

Desenvolvedores Node.js conhecem a interface do EventEmitter desde o primeiro dia, como uma ferramenta “sob o capô” de quase todos os módulos do Node. Especialmente quando temos funções que tem um tempo para acabar.

Resumo: usando o EventEmitter:

  • Você cria objetos observáveis com múltiplos listeners, em que cada um tem uma função de callback, i.e, cada listener tem um possível resultado.
  • Use emitter.emit(eventName[, …args]) ao invés de cb(null, data) para passar um resultado para uma função listener.
  • Use emit(‘error’, err) para passar um erro e fechar a execução.
  • Transmita múltiplas saídas de sua função.

O EventEmitter prove o popular on() método que insere funções em um objeto. Então, ele as invoca sincronamente uma a uma a medida que os eventos acontecem. Esta abordagem provê mais granularidade e controle do que a CPS, o que dá um resultado melhor.

Vamos expandir nossa função anterior readFiles() para filterFiles() a fim de fornecer uma maneira da mensagem de todos os consumidores assinantes encontrarem um arquivo durante uma pesquisa.

// Give a list of files all of them which match an extensionfunction findFiles (files, extension) {
const emitter = new EventEmitter()
if (files.length === 0) {
// yield an error
emitter.emit('error', 'no files supplied')
}
// Check for matches
function checkFiles () {
files.forEach(file => {
if (path.extname(file) === extension) {
// yield a result
emitter.emit('match', file)
}
})
}
// Ask the event loop to loop through our loop ...
process.nextTick(checkFiles)
// For chainability on on()
return emitter
}

Então, para usarmos esta função, temos de implementá-la assim:

findFiles(process.argv.slice(2), '.js')
.on('match', file => console.log(file + ' is a match'))
.on('error', err => console.log('Error emitted: ' + err.message))

Podemos também melhorar a forma, mas com a mesma funcionalidade:

'use strict' // Dependencies
const EventEmitter = require('events').EventEmitter
const path = require('path')
// Definition
class FindFiles extends EventEmitter {
constructor (extension) {
super()
this.extension = extension
this.files = []
}
addFile (file) {
this.files.push(file)
return this
}
// Check for matches
findFiles () {
process.nextTick(() => {
this.files.forEach(file => {
if (path.extname(file) === this.extension) {
this.emit('match', file)
}
})
})
return this
}}
// Instantiation of observable object
const FindFilesSearcher = new FindFiles('.js')

Se você já deu uma olhada no capítulo sobre o padrão observer neste famoso livro de padrões de projeto, você já viu a grande diferença entre a forma como implementamos este padrão. Eu acho a “forma do node.js” melhor — é mais simples e você pode expressar as mesmas ideias com menos código. E honestamente, eu ensinei este padrão para desenvolvedores web, e te garanto que eu tive melhor chance de sucesso relacionando a familiaridade com o popular método do jQuery, o on(), sem falar muito sobre abstrações e interfaces.

Combinando CPS e padrão Observer

Nossa nova função findFiles() é definitivamente mais flexível que a readFiles().

findFiles() provê uma forma do usuário ouvir o evento durante o processamento de cada arquivo, o que pode ser muito útil para controle do processamento.

Mas e se quisermos torná-lo ainda mais flexível, como deixar o usuário escolher se ele quer esse controle ou não? E se o usuário não está realmente interessado em cada arquivo, mas quer obter apenas o resultado final?

Bem, vamos juntar o EventEmitter com a função CPS:

'use strict' 
// Dependenciesconst
EventEmitter = require('events').EventEmitter
const path = require('path')
// Definitionfunction findFiles (files, extension, cb = null) {
const emitter = new EventEmitter()
const errorMessage = 'no files supplied';
if (files.length === 0) {
if (cb) {
cb(errorMessage)
}
emitter.emit('error', errorMessage)
}
if (cb) {
// cps
let result = []
for (let i = 0; i < files.length; ++i) {
if (path.extname(files[i]) === extension) {
result.push(files[i])
}
}
cb(null, result)
} else {
// event emitter style
process.nextTick(() => {
files.forEach(file => {
if (path.extname(file) === extension) {
emitter.emit('match', file)
}
})
})
return emitter
}}
// Implementation with a callback
findFiles(process.argv.slice(2), '.js', (err, result) => {
if (err) return console.error(err)
console.log(`All in one: ${result}`)
})
// Implementation with an eventmitter
findFiles(process.argv.slice(2), '.js')
.on('match', file => console.log(file + ' is a match'))
.on('error', err => console.log('Error emitted: ' + err.message))

Este é somente um exemplo de demonstração em que não se deve copiar para dentro do seu código sem ao menos checkar as possíveis entradas do usuário, etc. Contudo, o exemplo traz uma ideia bem básica em como podemos fazer argumentos de callback serem opcionais e usá-los dependendo do cenário.


Conclusão

Espero que no final deste artigo você tenha algumas idéias de alto nível sobre algumas opções que você tem ao projetar seus módulos do Node. É bom começar com o fim em mente, imaginando como você deseja que seus módulos sejam usados. Isso o ajudará a equilibrar melhor suas decisões entre o nível de flexibilidade e facilidade de uso que você deseja fornecer aos usuários.

E lembre-se — na maioria dos casos a escolha de uma linguagem de programação para uso é menos importante do que a forma como você usa a linguagem de programação.


É isso ai pessoal, espero que tenham gostado do texto. Se gostou de uma recomendação no texto, ou seja, clique no ícone de coração ;). Até a próxima, valeu!

Emanuel G de Souza

Written by

To go fast, go alone. To go far, go together.

EmanuelG Blog

Um blog pessoal, de quem gosta e ama tecnologias para a web. Aqui você encontra conteúdo dos mais variados sobre as principais tecnologias que cercam a internet.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade