Você tem uma função chamada addToCart que faz três coisas no mesmo lugar: adiciona o item a um array, chama uma API para persistir o carrinho e dispara um evento de analytics. O teste unitário, se existir, mocka as três coisas. Você muda uma e quebra as outras duas sem perceber. Esse é o problema que a programação funcional resolve, não com matemática abstrata, mas separando qual parte do seu código faz alguma coisa (efeito colateral) de qual parte só calcula alguma coisa (transforma dados).
Ações, cálculos e dados: a distinção que importa
Uma ação é qualquer função cujo resultado depende de quando ou quantas vezes ela é executada, ou que mexe em algo fora dela mesma. saveUser(user) que faz um POST na sua API é uma ação: chamá-la duas vezes cria dois registros, chamá-la sem rede falha. Date.now() também é uma ação, mesmo sem mudar nada: o resultado depende do momento exato em que roda.
Um cálculo é uma função pura: mesma entrada, mesma saída, sem mexer em nada de fora. const total = items => items.reduce((sum, item) => sum + item.price * item.qty, 0) é um cálculo. Não importa se você chama uma vez ou mil vezes, num teste ou em produção: para o mesmo array de items, o resultado é sempre o mesmo.
Os dados são os fatos, sem comportamento: { id: 'sku-104', price: 25000, qty: 2 }. Em JavaScript é tentador colocar comportamento neles com classes e métodos, mas quanto mais você os trata como estruturas inertes, mais fácil fica raciocinar sobre eles, serializá-los e compará-los num teste com um simples toEqual.
Refatorando uma ação de verdade
Voltando ao addToCart. É assim que ele começa, misturando as três coisas: function addToCart(item) { cart.push(item); localStorage.setItem('cart', JSON.stringify(cart)); analytics.track('item_added', { sku: item.id }); }. Essa função não dá para testar sem mockar localStorage e analytics, e também não dá para reaproveitar num contexto em que você ainda não quer persistir nada, por exemplo ao pré-carregar um carrinho vindo do servidor.
O primeiro passo é separar o cálculo do resto: o que é o carrinho novo, não como ele é persistido nem o que é rastreado. const addItem = (cart, item) => [...cart, item]. Essa versão é um cálculo puro: recebe o carrinho atual e o item, devolve um carrinho novo, sem mexer em nada externo. Você pode chamá-la mil vezes num teste sem configurar um único mock.
As ações ficam de fora, explícitas, na camada que orquestra: function handleAddToCart(item) { cart = addItem(cart, item); persistCart(cart); analytics.track('item_added', { sku: item.id }); }. persistCart e analytics.track continuam sendo ações, isso não muda, os efeitos colaterais existem porque o software precisa fazer coisas no mundo real. O que mudou é que o cálculo do novo estado do carrinho está isolado, é testável sem mocks, e pode ser reaproveitado em qualquer lugar em que você precise da mesma lógica sem arrastar os efeitos junto.
Onde a diferença aparece num sistema de verdade
Essa separação importa mais à medida que o sistema cresce. Um pipeline de checkout com validação de estoque, cálculo de impostos, aplicação de descontos e cálculo de frete fica gerenciável quando cada uma dessas etapas é um cálculo puro encadeado, e as ações (cobrar o cartão, atualizar o estoque, mandar o e-mail de confirmação) ficam nas bordas, rodando uma única vez no final, na ordem certa. Se você mistura tudo, cada mudança na lógica de descontos te obriga a checar se você quebrou a cobrança do cartão, porque estão na mesma função.
O mesmo critério vale no backend com lógica de negócio complexa, na transformação de dados de um pipeline de analytics, ou no estado de um frontend com Redux ou qualquer outro store: se você consegue tirar a lógica do que deveria acontecer de como isso é executado e com quais efeitos, o código resultante fica mais fácil de testar sem mocks, mais fácil de paralelizar sem condições de corrida, e mais fácil de ler porque cada função faz uma única coisa que cabe numa frase.
A programação funcional não te obriga a usar Haskell nem a abandonar classes por completo. A parte que realmente muda você como developer senior é mais simples e mais prática: antes de escrever uma função, pergunte a si mesmo se você está calculando algo ou fazendo algo, e se for a segunda opção, pergunte se isso realmente precisa estar misturado com o cálculo. Na maioria das vezes, não precisa.




