Reflexão em padrões de projeto para Node.js
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 …

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!

