DEV Community

Cover image for Funções Puras e imutabilidade, um pouco sobre código limpo e qualidade
Vinicius Reis for Codecasts

Posted on • Originally published at blog.codecasts.com.br

Funções Puras e imutabilidade, um pouco sobre código limpo e qualidade

Código limpo (clean code) é um tópico recorrente e importante. Infelizmente não há uma adoção tão grande quanto se imagina ou espera.

Entre as motivações estão a falta de aprofundamento do tópico e suas nuances, bem como uma boa contextualização e a realidade dos projetos ou empresas onde os profissionais de desenvolvimento de software trabalham.
O caos, a cobrança e demanda, muitos líderes ou gerentes não conseguem, ou simplesmente não veem valor nessas práticas, o bom e velho “o importante é funcionar”.

Talvez algumas pessoas fiquem chocadas ou surpresas ao saber que ainda há muitas empresas e profissionais não sigam práticas e conceitos consideradas por muitos básicas.

Este artigo visa ajudar a desmistificar alguns tópicos e técnicas, facilitando a adoção das mesmas.


Clean Code e JavaScript

Clean Code é um assunto extenso, até mesmo controverso em alguns aspectos. Algumas coisas podem ser consideradas subjetivas, debates não são raros.

Infelizmente muitos desses debates são bikeshedding, resultam em nada. Quando isto acontece, quem esta na posição de liderança do time precisa tomar uma postura rígida e clara, algumas vezes unilaterais. Não é algo simples, afinal pode gerar conflitos no time, porém, é algo necessário.

Meu conselho é: seja honesto e claro, a intenção não é impor uma vontade, mas sim encerrar diálogos improdutivos.

Ferramental

Muitas dessas discussões são sobre ponto e vírgula… Tabs ou espaços… CamelCase ou Snake Case… A solução é simples, adotar um estilo pré existente e consolidado e seguir com ele sem nenhuma alteração.

No JavaScript possuímos alguns padrões já bastante populares: Google Style Guide, Airbnb Style Guide e JavaScript Standard Style. Todos possuem suas regras para o ESLint. Escolha um deles e adote para seu time ou projeto, recomendo não fazer modificações nas regras.

Há plataformas que vão além do que o ESLint faz, analisando e classificando seu código. Algumas são gratuitas para projetos open-source.

Técnicas

São várias, este artigo vai se focar em Imutabilidade e Funções Puras. Porém, não podemos deixar de citar algumas como DRY, SOLID, KISS e Object Calisthenics.

Ao se aprofundar nessas técnicas você perceberá a convergência de ideias e fluxos, com o tempo e prática elas se tornaram naturais ao seu estilo de codificação.


Imutabilidade

Imutabilidade talvez seja um dos tópicos mais complexos deste artigo. Não uma complexidade técnica, mas uma complexidade quase filosófica e de alguma falsos positivos.

A primeira coisa que devemos entender, imutabilidade não significa constante.

const a = 73
a = 37

js const error

Este é o erro que recebemos ao executar o exemplo anterior, muitos vão assumir que const no JavaScript esta relacionado a constante, porém seu objetivo é impedir a reatribuição de uma variável.

Vamos entender um pouco mais como isso funciona.

Valores e Referências

Em JavaScript temos os seguintes primitivos:

Quando usamos os operadores de comparação == e === internamente e em resumo, estamos comparando referências de memória. JavaScript é uma linguagem 100% orientada a objetos, todos os seus tipos e valores são objetos.

Objetos vão possui um endereço de memória, na prática x == y esta comparando se o endereço de memória que x representa, é o mesmo que y representa. Por isso que {} == {} ou [] == [] não funciona, pois cada {} e [] apontam para locais diferentes da memória.

Sabendo que 2 é do tipo Number e também um objeto, como 2 == 2 funciona? A resposta é simples, os tipos primitivos em JavaScript são constantes e imutáveis.

01. let n = 73
02. n == 73 // true
03. n = 37
04. n == 73 // false
  • Na linha 01. declaramos n e atribuímos 73 para ele.
  • Na linha 02. comparamos n == 73, como n aponta para o mesmo endereço de memória que 73 temos true como resultado.
  • Na linha 03. reatribuímos n a outro valor, agora 37.
  • Na linha 04. comparamos n == 73, como n aponta para outro endereço e não para 73, temos false como resultado.

Todo primitivo em JavaScript se comporta dessa maneira, outra característica relacionada a este comportamento é a imutabilidade deles, métodos de um primitivo não modificam seu valor… não alteram sua referência na memória, quando necessário geram novas referências, mas nunca modificam a original.

const s1 = 'Codecasts'
const s2 = s1.toUpperCase() // CODECASTS
s1 == s2 // false

Este comportamento é nativo dos primitivos, porém outros tipos como Array e Object não se comportam dessa maneira, [7, 3] == [7, 3] sempre será false, pois cada um aponta para um endereço de memória diferente.

Explaining Value vs. Reference in Javascript

Referências Imutáveis

Quando estamos falando de imutabilidade em JavaScript estamos falando em evitar modificar referências.

Ao usar const em primitivos já estamos garantindo uma boa parcela de imutabilidade, porém o maior desafio é trabalhar com objetos e listas imutáveis.

01. const arr = [7, 3]
02. arr.push(12)           // arr --> [7, 3, 12]
03. arr.unshift(1)         // arr --> [1, 7, 3, 12]
04. const last = arr.pop() // arr --> [1, 7, 3]
05. last == 12 // true

Refs: .push, .unshift e .pop

Na linha 01. declaramos arr, atribuímos nele um array [7, 3]. Nas linhas 02. a 04. manipulamos arr e em seguida é ilustrado seu estado atual.

Inicialmente coisas assim podem parecer inofensivas, mas há um potencial de cenários extremamente difíceis de serem identificados ou rastreados.

O próximo exemplo ilustra como objetos são usados como referência.

const fn = x => x
const obj = {}
obj == fn(obj) // true

A função fn não faz nada além de retornar x seu único argumento. Ao comparar obj == fn(obj) temos true como resultado, pois estamos retornando a mesma instância/referência.

O exemplo abaixo explora brevemente os impactos desse comportamento.

const setPrice = product => {
  product.price = 73
}
const product = { name: 'Instant Kung Foo.' }
setPrice(product)
product.price == 73 // true

A função setPrice recebe product e adiciona ou modifica a propriedade price, isso fica claro quando comparamos product.price == 73. Isso se chama efeito colateral (side effect), a função setPrice é uma função impura.

const setPrice = product => {
  return { ...product, price: 73 }
}
const product = { name: 'Instant Kung Foo.' }
const newProduct = setPrice(product)
product.price == 73 // false
newProduct.price == 73 // true

Agora a função setPrice retorna uma cópia rasa (shallow copy) de product que recebeu como argumento, acrescentando ou modificando a propriedade price. Esse procedimento obriga a criação de uma nova variável newProduct, isso acontece pois const foi utilizado, o próximo exemplo usa let.

Este fluxo pode incomodar um pouco, normalmente esta abordagem não é necessária, usa-se técnicas como pipe e compose.

Funções Puras

Funções puras (pure functions) andam ao lado da imutabilidade, na prática podemos dizer que uma não existe sem a outra.

Entende-se como pura a função que sempre devolve o mesmo resultado para um mesmo argumento, não importando a quantidade de vezes que ela é chamada, sem causar efeitos coletareis (side effects).

Parece e é obvio, porém, infelizmente ainda causa dúvidas para alguns programadores. Essa técnica exige um pouco de esforço por parte do programador, ele tem que explicitamente optar por usa-la.
Vajamos o seguinte exemplo:

// impure.js
let counter = 0
const addToCounter =  value => {
  counter = counter + value
}
// pure.js
const sum = (x, y)  => x + y
let counter = 0
counter = sum(counter, 10)

Este é um dos primeiros exemplos usados para descrever uma função impura, é bem óbvio. Porém sua simplicidade também dificulta sua adoção, sabemos que um programa real é mais complexo do que essa função.

const request = params => {
  if (params.client) {
    params.client_id = params.client.id
    delete params.client
  }
  return http.get('/', params)
}

O exemplo anterior é mais próximo a uma aplicação real. Temos a função request que recebe params, caso params.client exista, params é modificado.

A função request gera um efeito colateral gravíssimo, é impossível determinar a instabilidade e consequentemente bugs que essa função causa para quem a chamar.

const request = params => {
  const options = { ...params }

  if (options.client) {
    options.client_id = options.client.id
    delete options.client
  }
  return http.get('/', options)
}

Com esta mudança o problema foi resolvido, params não é mais modificado, o procedimento agora é aplicado a uma cópia dele. Porém, ainda há como melhorar esta função.

const parseRequestParams = params => {
  const data = { ...params }
  if (data.client) {
    data.client_id = data.client.id
    delete data.client
  }
  return data
}
const request = params => {
  const options = parseRequestParams(params)
  return http.get('/', options)
}

Toda a lógica de tratamento dos parâmetros foi isolada em parseRequestParams. A função passa a ser mais objetiva, delegando parte da lógica a outra função, uma função mais especializada. Isso não apenas melhora a leitura como permite criar testes de unidade com maior cobertura de possibilidades.

Uma linha, um comando, uma função.

A função parseRequestParams ainda pode evoluir ao utilizar técnicas com pipe e compose. A biblioteca ramda fornece essas e várias funções úteis para ajudar na implementação dessas técnicas.

import { compose, omit } from 'ramda'

const parseClientInRequestParams = params => {
  if (!params.client) {
    return params
  }

  return {
    ...omit(['client'], params),
    client_id: params.client.id
  }
}

const parseUserInRequestParams = params => {
  if (!params.user) {
    return params
  }

  return {
    ...omit(['user'], params),
    user_id: params.user.id
  }
}

const parseRequestParams = compose(parseClientInRequestParams, parseUserInRequestParams)

As funções parseClientInRequestParams e parseUserInRequestParams ficaram muito similares, podemos simplifica-las.

import { compose, omit, has, path } from 'ramda'
const createParseEntityInRequestParams = key => {
  return params => {
    if (!has(key, params)) {
      return params
    }
    const id = path([key, 'id'], params)
    return {
      ...omit([key], params),
      [`${key}_id`]: id
    }
  }
}
const parseClientInRequestParams = createParseEntityInRequestParams('client')

const parseUserInRequestParams = createParseEntityInRequestParams('user')

A função createParseEntityinRequestParams retorna uma função que funciona exatamente como as implementações anteriores de parseClientInRequestParams e parseUserInRequestParams.

Receber e retornar funções só é possível pois funções são Cidadãos de Primeira-Classe (First-class citizens) no JavaScript, são tratadas como valores.

Com o tempo mais padrões desses vão surgindo e se torna natural o uso de funções puras e factory functions. É um nível de reuso muito grande e de extrema flexibilidade.

Encapsulando Fluxos Impuros

Infelizmente não é viável criar uma aplicação que não lide com fluxos impuros. Operações que dependem de i/o, disco, rede, banco de dados… São operações com impuras e muitas vezes vão produzir efeitos colaterais.

Quando falamos de programação funcional, não é raro termos como monads aparecerem. Eles dão origem a muita confusão, em especial para quem ainda esta apenas começando com programação funcional.

Este artigo não vai tratar de monads, e sim de uma abordagem simplista para essas situações.

const request = params => {
  const options = parseRequestParams(params)
  return http.get('/', options)
}

Reaproveitando o exemplo anterior, temos a função request. Por mais que ela não gera mais um efeito colateral direto ela retorna uma promessa. Existe uma discussão sobre a “pureza” de uma função que retorna uma promessa, a verdade é que ela poderá ter resultados diferentes mesmo que os argumentos sejam os mesmos, isso torna ela uma função impura.

Sendo ela uma função pura ou não, há um problema mais “grave”, o fato dessa função depender de http para funcionar. Assumimos que http é uma global, provavelmente uma instância do axios. Justamente por ser global que temos um problema na hora de testar esta função.

Depender de estados globais prejudica muito o processo de testes, pois é necessário recriar toda uma camada de ambiente para que o teste seja possível, além de usar técnicas sofisticadas de mocking.

A solução é simples, injetar http em request.

const request = (http, params) => {
  const options = parseRequestParams(params)
  return http.get('/', options)
}

Isso pode ser particularmente verboso na hora de utilizar, porém com técnicas de injeção de dependências isso passa a ser simples e prático. Os links abaixo demonstram algumas técnicas que vão ajudar.


Este artigo possui um objetivo subliminar, introduzir o leitor a programação funcional sem que ele perceba. Todo o conteúdo aqui apresentado faz parte das técnicas e fluxos de programação funcional com JavaScript.

Quando se fala de programação funcional muito se fala de lambda, functors, monads e outros termos… Programação funcional possui diversas camadas e convive bem com outros paradigmas.

Seguir os princípios da programação funcional é uma das melhores maneiras de se obter um código limpo e de qualidade.

Em minha concepção pessoal um código de qualidade é um código testável. Talvez a maior dificuldade em se implementar testes seja justamente escrever código testável. Imutabilidade e funções puras são os primeiros passos.


Este artigo foi originalmente postado em 2018-12-30, no Medium


Se quiser saber mais sobre meu trabalho visite dev.to/codecasts ou blog.codecasts.com.br. Assine nosso canal no YouTube, lá você vai ver vídeos sobre JavaScript, jQuery, Gulp, ES6, Vue.JS e muito mais. Também não deixe de entrar em contato pelo nosso grupo no Telegram

Top comments (0)