DanLevy.net

Maestro delle pipeline: Trasferimento dello stato

Ciao Closure, mio vecchio amico.

Hero image for Maestro delle pipeline: Trasferimento dello stato

Maestro delle Pipeline: Passaggio dello Stato

Ti è mai capitato di avere difficoltà a passare lo stato attraverso pipeline funzionali?

L’organizzazione (o la sua mancanza) del tuo codice influisce direttamente sulla facilità con cui lo stato viene passato.

In questo articolo esploreremo una tecnica efficace per passare lo stato attraverso una pipeline. Lungo il percorso miglioreremo l’organizzazione e la leggibilità del nostro codice.

Il seguente frammento “reale” sarà il nostro focus per questo articolo: una funzione di checkout, che accetta un userId e un array di products. Restituisce una catena di Promise che esegue 4 funzioni in sequenza.

const checkout = (userId: number, products: number[]) => {
return getProductsSubtotal(userId, products)
.then(subTotal => applyTaxes(userId, subTotal))
.then(total => purchaseProducts(userId, total))
.then(result => sendReceipt(userId, result));
};

Un attimo, questo codice è in realtà piuttosto decente, per quanto riguarda le pipeline in JS!

Soffre di alcuni problemi sottili che possono combinarsi in problemi più sostanziali.

Un problema è che stiamo passando ripetutamente userId a ogni funzione (logicamente correlata). Ora combina questo con un altro problema che è facilmente trascurato dagli sviluppatori e anche da TypeScript: invertire gli argomenti numerici crea facilmente un bug silenzioso. (Vedi applyTaxes e purchaseProducts. Era userId o amount il primo parametro?)

Prima di decidere come migliorare questo codice, identifichiamo alcuni pro e contro.

Pro e Contro

Pro

Contro

Soluzione, Parte 1: Crea un modulo!

Questa tecnica consiste nell’organizzare funzioni correlate in un unico modulo (es. CartHelpers). Non richiede un pattern specifico. Esplora factory functions, Classi, Closure, Mixin, ecc. Trova ciò che ha senso per il tuo progetto e team.

CartHelpers Factory

Esempio di un modulo CartHelpers, dove userId viene passato una volta sola e tutti i metodi sono a singolo argomento.

const CartHelpers = (userId: number) => {
return {
getProductsSubtotal: products => getProductsSubtotal(userId, products),
applyTaxes: subTotal => applyTaxes(userId, subTotal),
purchaseProducts: total => purchaseProducts(userId, total),
sendReceipt: invoice => sendReceipt(userId, invoice)
};
};

Classe CartHelpers

Se le classi sono il tuo forte, è facile adattarle:

class CartHelpers {
constructor(userId) {
this.userId = userId;
}
getProductsSubtotal = products => getProductsSubtotal(this.userId, products);
applyTaxes = subTotal => applyTaxes(this.userId, subTotal);
purchaseProducts = total => purchaseProducts(this.userId, total);
sendReceipt = invoice => sendReceipt(this.userId, invoice);
}

Alcuni vantaggi immediati:

Raggruppando funzioni correlate, si crea l’opportunità di ridurre la superficie esposta (es. checkout(), metodi ‘pubblici’ di CartHelpers).

Meno superficie === meno carico cognitivo, testabilità e manutenibilità migliori. Progetta sistemi con intenzione e focus. ✨

Utilizzo di Checkout e CartHelpers

Vediamo come appare ora la funzione checkout():

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
return Promise.resolve(products)
.then(products => cart.getProductsSubtotal(products))
.then(subTotal => cart.applyTaxes(subTotal))
.then(total => cart.purchaseProducts(total))
.then(result => cart.sendReceipt(result));
};
Checkout con ulteriori miglioramenti

Si può migliorare ulteriormente? Sì! Non dobbiamo ripetere affatto gli argomenti!

Quando gli argomenti di una funzione sono forniti dall’output di funzioni precedenti, puoi semplificare ulteriormente il codice.

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
// 🌈 Le funzioni si impilano come Lego e si leggono come "Parole Umane!" 💅
return Promise.resolve(products)
.then(cart.getProductsSubtotal)
.then(cart.applyTaxes)
.then(cart.purchaseProducts)
.then(cart.sendReceipt);
};

Se sembra innaturale combinare parametri in un singolo argomento (oggetto), considera di suddividere le funzioni OPPURE combinarle in moduli con uno scopo più appropriato.

Da dove iniziare?

Trova funzioni correlate e raggruppale insieme. (es. CartHelpers.)

Parte della sfida nell’individuare possibili moduli logici è identificare il codice correlato già in partenza.

Cosa rende le funzioni correlate?

Un trucco efficace: trova ripetizioni nei parametri delle funzioni. Chiediti: c’è una relazione in gioco? O una responsabilità sottostante?

Sebbene non esista un’unica “risposta giusta” per progettare moduli, è utile identificare 2-3 opzioni di organizzazione: tracciare uno schema, scrivere codice “fantasy”, chiedersi “suscita gioia?”

Potresti pensare che cart.sendReceipt() non appartenga ai metodi legati ai pagamenti. Forse customerNotifications.sendReceipt() è una collocazione migliore per la messaggistica ai clienti. Se CartHelper è abbastanza importante, potrebbe agire come un controller che chiama internamente tutti i servizi necessari, come customerNotifications.

Come capire se stai aiutando?

Se la leggibilità non ne risente mentre elimini argomenti ad-hoc, CONGRATULAZIONI!!! Probabilmente hai costruito un modulo con un ambito chiaro e durevole!

Allora, sorge spontanea la domanda: dove aggiungiamo funzionalità?

Per esperienza, ci sono 2 strategie principali da valutare quando si aggiunge funzionalità:

  1. Estendere/rifattorizzare il metodo esistente. (Quando il nuovo codice è sufficientemente vicino a quello esistente.)
  2. Creare una nuova (5ª) funzione nel punto desiderato della catena. (Supponendo che il nuovo codice non sia correlato alle funzioni esistenti.)

In definitiva, questo semplifica la decisione su dove collocare le nuove funzionalità. (Ad esempio, cart.applyDiscounts(), cart.applyTaxes(), rewards.getBalance().)

Conclusione

Passare lo stato attraverso una pipeline complessa può essere insidioso. Tuttavia, con un po’ di pratica nel refactoring, ti ritroverai a scrivere codice più leggibile, con un carico cognitivo minore.

Domande? Commenti? Preoccupazioni? Sentiti libero di contattarmi su @justsml o via email.

Resta sintonizzato per la prossima parte della serie

Esploreremo l’esternalizzazione dello stato e l’estensione delle funzionalità nel nostro modulo!

Letture correlate