DanLevy.net

Maestro de pipelines: paso de estado

Hola Closure, mi viejo amigo.

Hero image for Maestro de pipelines: paso de estado

Maestro de los Pipelines: Pasando Estado

¿Te has encontrado con dificultades al pasar estado usando Pipelines Funcionales?

La organización (o la falta de ella) de tu código impacta directamente en la facilidad con la que el estado se propaga.

En este artículo exploraremos una técnica eficaz para pasar estado a través de un pipeline. En el proceso mejoraremos la organización y la legibilidad de nuestro código.

El siguiente fragmento “real” será nuestro foco para este artículo: una función de checkout, que acepta un userId y un arreglo de products. Devuelve una cadena de Promises que ejecuta 4 funciones en secuencia.

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));
};

Espera un momento, ¡este código está bastante decente, considerando los pipelines en JS!

Presenta algunos problemas sutiles que pueden combinarse y convertirse en fallos más graves.

Un inconveniente es que seguimos pasando userId a cada función (lógicamente relacionada).
A eso se suma otro detalle que los desarrolladores y TypeScript pasan por alto con facilidad: invertir el orden de los argumentos numéricos genera un error silencioso. (Véanse applyTaxes y purchaseProducts. ¿Era userId o amount lo que debía ir primero?)

Antes de decidir cómo mejorar este código, identifiquemos algunos pros y contras.

Pros & Cons

Pros

Contras

Solución, Parte 1: ¡Crear un módulo!

Esta técnica consiste en organizar funciones relacionadas en un único módulo (p. ej., CartHelpers). No obliga a un patrón específico. Explora funciones fábrica, Clases, cierres, mixins, etc. Encuentra lo que tenga sentido para tu proyecto y tu equipo.

Fábrica CartHelpers

Ejemplo de un módulo CartHelpers, donde userId se pasa una sola vez y todos los métodos son de un solo argumento.

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)
};
};

Clase CartHelpers

Si prefieres clases, es fácil adaptarlo:

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);
}

Algunos beneficios inmediatos:

Al agrupar funciones relacionadas, creamos la oportunidad de reducir la superficie expuesta (p. ej., checkout(), métodos “públicos” de CartHelpers).

Menos superficie === menos carga cognitiva, mejores pruebas y mantenibilidad.
Diseña sistemas con intención y foco. ✨

Checkout y Uso de CartHelpers

Veamos cómo se ve ahora la función 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 mejoras adicionales

¿Se puede mejorar más? ¡Sí! No tenemos que repetir argumentos en absoluto.

Cuando los argumentos de una función provienen de la salida de funciones anteriores, puedes simplificar el código aún más.

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
// 🌈 ¡Las funciones se apilan como Lego y se leen como palabras humanas normales! 💅
return Promise.resolve(products)
.then(cart.getProductsSubtotal)
.then(cart.applyTaxes)
.then(cart.purchaseProducts)
.then(cart.sendReceipt);
};

Si te resulta antinatural combinar parámetros en un solo argumento (objeto), considera dividir tus funciones O agruparlas en módulos con un alcance más apropiado.

¿Por dónde empezar?

Encuentra funciones relacionadas y agrúpalas. (p. ej., CartHelpers.)

Parte del reto al buscar posibles módulos lógicos es identificar el código relacionado en primer lugar.

¿Qué hace que las funciones estén relacionadas?

Un truco útil: busca repeticiones en los parámetros de las funciones. Pregúntate si hay una relación subyacente o una responsabilidad compartida.

Mientras no exista una única “respuesta correcta” para diseñar módulos, resulta útil identificar de 2 a 3 opciones de organización: dibujar un esquema, escribir código “fantasía”, preguntar “¿esto genera alegría?”.

Puede que sientas que cart.sendReceipt() no encaja con los métodos relacionados con pagos. Tal vez customerNotifications.sendReceipt() sea un mejor lugar para la mensajería al cliente. Si CartHelper tiene suficiente importancia, podría actuar como un controlador interno que invoque a todos los servicios necesarios, como customerNotifications.

¿Cómo saber si estás aportando valor?

Si la legibilidad no se ve afectada al eliminar argumentos ad‑hoc, ¡FELICITACIONES! Probablemente hayas construido un módulo con un alcance claro y duradero.

Así que surge la pregunta: ¿dónde añadimos la funcionalidad?

En mi experiencia hay 2 estrategias principales a evaluar al incorporar nueva funcionalidad:

  1. Extender/refactorizar el método existente. (Cuando el nuevo código está lo suficientemente cerca del código actual.)
  2. Crear una nueva (quinta) función en el punto deseado de la cadena. (Suponiendo que el nuevo código no está relacionado con las funciones existentes.)

En última instancia, esto facilita decidir a qué lugar pertenece la nueva funcionalidad. (p. ej., cart.applyDiscounts(), cart.applyTaxes(), rewards.getBalance().)

Conclusión

Pasar estado a través de una canalización compleja puede ser complicado. Sin embargo, con un poco de práctica de refactorización, terminarás escribiendo código más legible y con menos carga cognitiva.

¿Preguntas? ¿Comentarios? ¿Inquietudes? No dudes en contactar a @justsml o por correo electrónico.

Mantente atento a la próxima entrega de la serie

Exploraremos cómo externalizar el estado y ampliar la funcionalidad en nuestro módulo.

Lecturas relacionadas