If you cannot read in Portuguese, try to use the Google Translator. Each code sample here will be written in English. Feel free to translate this article and share.
No parte 1 deste artigo, partimos de um for
simples de somatória…
const lineItems = [
{ name: "Carmenere", price: 35, quantity: 2 },
{ name: "Cabernet", price: 50, quantity: 1 },
{ name: "Merlot", price: 98, quantity: 10 }
];
const sumTotal = lines => {
let total = 0;
for (let i = 0; i < lines.length; i++) {
total += lines[i].quantity * lines[i].price;
}
return total;
};
console.log(sumTotal(lineItems)); // 1100
… e refatoramos com um reduce
e um pouco de separação de responsabilidades. Ficou assim:
const lineItems = [
{ name: "Carmenere", price: 35, quantity: 2 },
{ name: "Cabernet", price: 50, quantity: 1 },
{ name: "Merlot", price: 98, quantity: 10 }
];
const totalLine = line => line.quantity * line.price;
const sumTotal = lines =>
lines.reduce((total, line) => (total += totalLine(line)), 0);
console.log(sumTotal(lineItems));
Agora seguimos com nossa refatoração separando ainda mais as responsabilidades, identificando padrões e elaborando códigos genéricos que possam ser reutilizados facilmente. Vamos lá!
Dividindo para conquistar
Podemos melhorar nosso código agora separando em funções diferentes o momento de cálculo de uma linha e o cálculo final:
const totalLine = line => line.quantity * line.price;
const sum = (a, b) => a + b;
const sumTotal = lines => lines.map(totalLine).reduce(sum, 0);
console.log(sumTotal(lineItems));
const sum = (a, b) => a + b
https://twitter.com/unclebobmartin/status/360029878126514177
Note que a
, b
e xs
não parecem bons nomes de variável, certo? Enquanto line
parece um nome bem adequado e preciso em seu significado. As variáveisa, b e xs são todas de escopos limitadíssimos e suas implementações são um tanto genéricas. São nomes imparciais. Já o argumento line participa de uma implementação bem mais específica, apesar do escopo igualmente limitado, é suposto que line
possua quantity
e price
, e ambos possam ser multiplicados entre si. Portanto, esse é o motivo de nomes genéricos e nomes específicos. Trata-se de níveis de abstração.
Mas, por que precisamos de uma função de soma se já temos o + nativo do JavaScript? Diferente de linguagens como Ruby, no JavaScript o +
não equivale a uma função comum (em Ruby +
é método), eu não consigo manuseá-la como qualquer outra: enviar como argumento, receber como retorno ou mesmo verificar aridade e outros atributos comuns de uma função. Tampouco posso sobrescrevê-la! Isso acontece porque o +
não é uma função, é um operador. Nossa função de soma simplesmente reescreve o operador + como uma função. A vantagem desta abordagem é que agora podemos alimentar o reduce
com nossa nova função +
.
(curiosidade: em JavaScript podemos verificar a aridade de uma função assim, fn.length
, veja mais detalhes no MDN)
map = fn => xs =>
O próximo passo é reescrever o map e o reduce!
— Calma, mas por que diabos vou reescrever uma função nativa (mais uma!)?
— Porque ela não permite composição direta.
Para que o map
e o reduce
permitam composição direta vou precisar de duas coisas:
Acessá-las antes de conhecer o dado que irá ser recebido (pois a composição que irá fornecer os dados).
Modificá-las com uso de uma outra função base antes de conhecer o dado que irá ser recebido.
Em outras palavras preciso alimentá-la com seus dois argumentos em dois momentos diferentes.
Nativamente só temos acesso à função map
quando já temos um array: [].map, [1, 2, 3, 4].map
. Podemos resolver isso utilizando o protótipo do map
: Array.prototype.map
, mas ainda vamos precisar apontar para esse map a função de mapeamento antes de passar o dado. Portanto: Array.prototype.map.call(dado, fn)
não nos serve.
Primeiro preciso desatrela-la do dado, depois preciso escrevê-las em dois passos; dois momentos. No primeiro, ela recebe a função de operação (fn
), em seguida recebe o dado. Para isso, escrevo de forma que uma função retorne outra. A sintaxe do =>
ajuda bastante neste tipo de escrita.
Acho que mostrando o código fica mais fácil de entender o que preciso:
const totalLine = line => line.quantity * line.price;
const sum = (a, b) => a + b;
const map = fn => xs => xs.map(fn);
const reduce = (fn, ini) => xs => xs.reduce(fn, ini);
const sumTotal = lines => reduce(sum, 0)(map(totalLine)(lines));
console.log(sumTotal(lineItems));
const map = fn => xs => xs.map(fn);
const reduce = (fn, ini) => xs => xs.reduce(fn, ini);
A minha função _map_
recebe apenas uma função e retorna uma outra função que exige apenas um argumento, que é o dado que vamos processar. Ou seja: map(totalLines)
irá retornar xs => xs.map(totalLines)
. Assim, posso chamar as duas funções em dois momentos, map(totalLines)(lines)
:
Ao chamar a última função enviando o
xs
, omap
é processado e retorna seu valor final.
Já a função reduce
recebe dois argumentos, a função de processamento e o valor inicial, de retorno ela entrega uma outra função que recebe somente o dado a ser processado. Ao final teremos: reduce(sum, 0)(lines)
. Nossa intenção aqui é padronizar a entrada de nossas funções para que a entrada de uma seja a saída da outra, por isso é importante termos funções com apenas um único argumento de entrada.
Repare que tenho duas funções exigindo somente um argumento, lines
:
map(totalLines)(lines)
reduce(sum, 0)(lines)
Podemos melhorar a leitura dando um nome adequado para map(totalLines)
e outro para reduce(sum, 0)
, dessa forma teremos duas novas funções. Lembrando que cada uma delas tem aridade 1, ou seja, esperam somente um argumento:
const totalLine = line => line.quantity * line.price;
const sum = (a, b) => a + b;
const map = fn => xs => xs.map(fn);
const reduce = (fn, ini) => xs => xs.reduce(fn, ini);
const calculateTotals = map(totalLine);
const sumAll = reduce(sum, 0);
const sumTotal = lines => sumAll(calculateTotals(lines));
console.log(sumTotal(lineItems));
const calculateTotals = map(totalLine);
const sumAll = reduce(sum, 0);
Perceba como o dado flui por entre as funções:
lines
|
const sumTotal = ( | ) =>
|
<---- sumAll( <--- calculateTotals( <--+ ))
_Uma peça de LEGO funciona devido a sua parte inferior ser encaixável na sua parte inferior ou ao contrário. Funções capazes de compor precisam ter encaixes similares.
São como peças de LEGO, a entrada de uma se encaixa perfeitamente na saída da outra. Esse é o conceito básico das composições. Como só há possibilidade de retornar um único valor, o encaixe mais simples e viável são entre funções unárias, ou seja, que recebem somente um único argumento. Existem composições mais complexas e específicas envolvendo vários argumentos de entrada, como é o caso das composições de reducers do Redux, por exemplo. Nesse caso, um dos argumentos é repassado inteiro para todas as funções que fazem parte da composição. Mas esse tipo de composição não será o foco aqui.
Lógica genérica e lógica de negócio
A essa altura está muito claro o que é lógica de negócio e o que não é. Vou separar esses dois tipos de código para facilitar ainda mais:
// generic logic
const sum = (a, b) => a + b;
const map = fn => xs => xs.map(fn);
const reduce = (fn, ini) => xs => xs.reduce(fn, ini);
const sumAll = reduce(sum, 0);
// business logic
const totalLine = line => line.quantity * line.price;
const calculateTotals = map(totalLine);
const sumTotal = lines => sumAll(calculateTotals(lines));
console.log(sumTotal(lineItems));
Perceba o nome das funções e argumentos. Enquanto nosso código genérico, de biblioteca é bem genérico, o nosso código específico diz um pouco sobre o contexto onde ele é aplicado. Isso não é sobre programação funcional, esse conceito pode ser utilizado em qualquer paradigma de programação. A separação fica mais evidente quando as responsabilidades estão bem divididas. E, sem dúvida esse é um dos bons motivos para levar muito a sério o princípio de responsabilidade única.
A camada mais genérica do meu código praticamente nunca precisará de manutenção, apenas em caso de bugs. Já a camada de negócio, poderá ser remodelada o quanto quiser. Garantir a camada genérica simples e funcionando bem é um bom caminho para evitar bugs.
Uma coisa que sempre faço em meus códigos é experimentar a sonoridade dele. Leia cada função da nossa camada de negócio em voz alta:
total da linha é: dado uma linha, quantidade da linha multiplicado pelo preço da linha.
cálculo de totais é um mapeamento dos totais das linhas.
soma total é: dado uma linha, a soma de todos os cálculos de totais desta linha.
Viu só?
O que faremos com essas duas camadas de código? Vejamos na última parte deste artigo, quero ainda mostrar finalmente as composições, tubulações e ainda um pouco de curry e o real motivo de termos reescrito (ou remodelado) tantas funções nativas. Vamos nessa?
Thanks to Igor Marques da Silva.
We want to work with you. Check out our Services page!