O objetivo deste artigo é ilustrar por que a programação reativa é necessária, como ela se relaciona com a programação funcional e como usá-la para escrever código declarativo que é fácil de adaptar a novos requisitos. Além disso, quero fazer isso da maneira mais rápida e simples possível, por um exemplo próximo do real.
Execute a seguinte tarefa:
Há um determinado serviço com API REST e ponto de extremidade /people
. Uma solicitação POST para este terminal cria uma nova entidade. Escreva uma função que utilize uma matriz de objetos no formato { name: 'Max' }
e crie um conjunto de entidades usando a API (em inglês, isso é chamado de operação em lote).
Vamos resolver esse problema em um estilo imperativo:
const request = require('superagent') function batchCreate(bodies) { const calls = [] for (let body of bodies) { calls.push( request .post('/people') .send(body) .then(r => r.status) ) } return Promise.all(calls) }
Para comparação, vamos reescrever esse pedaço de código em um estilo funcional. Por simplicidade, entendemos por estilo funcional:
- Uso de primitivas funcionais ( .map , .filter , .reduce ) em vez de loops imperativos ( por enquanto )
- O código está organizado em funções "puras" - eles dependem apenas de seus argumentos e não dependem do estado do sistema
Código de estilo funcional:
const request = require('superagent') function batchCreate(bodies) { const calls = bodies.map(body => request .post('/people') .send(body) .then(r => r.status) ) return Promise.all(calls) }
Temos um código do mesmo tamanho e vale a pena confessar que não está claro como essa peça é melhor que a anterior.
Para entender por que o segundo pedaço de código é melhor - você precisa começar a mudar o código, imagine que um novo requisito tenha aparecido para a tarefa original:
O serviço que chamamos tem um limite no número de solicitações em um período de tempo: em um segundo, um cliente pode executar não mais que cinco solicitações. A execução de mais solicitações fará com que o serviço retorne um erro HTTP 429 (muitas solicitações).
Neste ponto, provavelmente vale a pena parar e tentar resolver o problema sozinho,% nome de usuário%
Vamos tomar nosso código funcional como base e tentar alterá-lo. O principal problema da programação funcional "pura" é que ela não "sabe" nada - sobre o tempo de execução e a entrada e saída (em inglês há uma expressão para efeitos colaterais ), mas na prática trabalhamos constantemente com eles.
Para preencher essa lacuna, a Programação Reativa vem em socorro - um conjunto de abordagens que tentam resolver o problema dos efeitos colaterais. A implementação mais famosa desse paradigma é a biblioteca Rx , usando o conceito de fluxos reativos .
O que são fluxos reativos? Se for muito breve, essa é uma abordagem que permite aplicar primitivas funcionais (.map, .filter, .reduce) a algo distribuído ao longo do tempo.
Por exemplo, enviamos um determinado conjunto de comandos pela rede - não precisamos esperar até obtermos todo o conjunto, apresentamos como um fluxo reativo e podemos trabalhar com ele. Dois outros conceitos importantes surgem aqui:
- o fluxo pode ser infinito ou arbitrariamente distribuído ao longo do tempo
- o lado emissor envia o comando apenas se o lado receptor estiver pronto para processá-lo (contrapressão)
O objetivo deste artigo é encontrar maneiras fáceis; portanto, usaremos a biblioteca Highland , que tenta resolver o mesmo problema que o Rx, mas é muito mais fácil de aprender. A idéia é simples: vamos tomar os fluxos do Node.js como base e "transferir" os dados de um fluxo para outro.
Vamos começar: vamos começar com um simples - tornaremos nosso código "reativo" sem adicionar novas funcionalidades
const request = require('superagent') const H = require('highland') function batchCreate(bodies) { return H(bodies) .flatMap(body => H(request .post('localhost:3000/people') .send(body) .then(r => r.status) ) ) .collect() .toPromise(Promise) }
O que você deve prestar atenção:
- H (corpos) - criamos um fluxo a partir de uma matriz
- .flatMap e o retorno de chamada que ele aceita. A idéia é bastante simples - envolvemos o Promise em um construtor de fluxo para obter um fluxo com um valor (ou um erro. É importante entender que esse é o valor, não o Promise).
Como resultado, isso nos fornece um fluxo de fluxos - usando o flatMap, nós o suavizamos em um único fluxo de valores nos quais podemos operar (quem disse que a mônada?) - .collect - precisamos coletar todos os valores em um "ponto" em uma matriz
- .toPromise - retornará para nós Promise, que será cumprida no momento em que tivermos um valor do fluxo
Agora vamos tentar implementar nosso requisito:
const request = require('superagent') const H = require('highland') function batchCreate(bodies) { return H(bodies) .flatMap(body => H(request .post('localhost:3000/people') .send(body) .then(r => r.status) ) ) .ratelimit(5, 1000) .collect() .toPromise(Promise) }
Graças ao conceito de contrapressão, essa é apenas uma linha que está limitada neste paradigma. No Rx, ocupa quase a mesma quantidade de espaço .
Bem, isso é tudo, sua opinião é interessante:
- Consegui alcançar o resultado declarado no início do artigo?
- É possível alcançar um resultado semelhante usando uma abordagem imperativa?
- Você está interessado em programação reativa?
PS: aqui você pode encontrar outro artigo meu sobre Programação Reativa