DanLevy.net

精通 Pipeline:状态传递

你好闭包,我的老朋友。

Hero image for 精通 Pipeline:状态传递

管道大师:状态传递

你是否在利用函数式管道(Functional Pipelines)传递状态时遇到过挑战?

代码的组织方式(或其混乱程度)直接影响了状态传递的难易程度。

在本文中,我们将探讨一种在管道中传递状态的有效技术。在此过程中,我们还将提升代码的组织结构和可读性。

以下这段“真实”的代码片段将是本文的重点:一个结账函数,它接收 userId 和一个 products 数组,并返回一个按顺序执行 4 个函数的 Promise 链。

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

等等,就 JS 的管道而言,这段代码其实写得还不错!

但它确实存在一些微妙的问题,这些问题累积起来可能会演变成更严重的麻烦。

其中一个问题是,我们不断地向每个(逻辑相关的)函数重复传递 userId。 再结合另一个开发者和 TypeScript 都容易忽略的问题:调换数值参数的顺序很容易产生隐蔽的 Bug。(看看 applyTaxespurchaseProducts到底是 userId 在前还是 amount 在前?

在决定如何改进这段代码之前,我们先来分析一下它的优缺点。

优缺点分析

优点

缺点

解决方案第一部分:构建模块!

这种技术的核心是将相关的函数组织到一个模块中(例如 CartHelpers)。它不强制要求特定的模式。你可以尝试 工厂函数类(Classes)、闭包、混入(Mixins)等。选择最适合你的项目和团队的方式。

CartHelpers 工厂函数

这是一个 CartHelpers 模块的示例,其中 userId 只需传入一次,且所有方法都是单参数的。

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

CartHelpers 类

如果你更倾向于使用类(Class),也很容易适配:

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

显而易见的好处:

通过将相关函数分组,我们有机会减少暴露的表面积(例如 checkout() 内部只关注 CartHelpers 的“公共”方法)。

更小的表面积 === 更低的认知负荷,更好的测试与可维护性。 有意识、有重点地设计系统。✨

Checkout 与 CartHelpers 的配合使用

来看看现在的 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

还能再优化吗?当然!我们甚至可以完全不重复写参数!

当一个函数的参数直接由前一个函数的输出提供时,你可以进一步简化代码。

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
// 🌈 函数像乐高一样堆叠,读起来就像普通的“人话”!💅
return Promise.resolve(products)
.then(cart.getProductsSubtotal)
.then(cart.applyTaxes)
.then(cart.purchaseProducts)
.then(cart.sendReceipt);
};

如果你觉得将参数组合成单个(对象)参数很不自然, 请考虑拆分你的函数,或者将它们组合进作用域更合适的模块中。

从哪里开始?

寻找相关的函数,并将它们归类(例如 CartHelpers)。

寻找潜在逻辑模块的难点之一,首先在于识别哪些代码是相关的。

什么是函数的相关性?

一个实用的技巧:寻找函数参数中的重复项。问问自己:这里是否存在某种关联?或者某种底层的职责?

虽然设计模块没有唯一的“标准答案”,但识别出 2-3 种组织方案会很有帮助——画个大纲,写写“幻想”代码,然后问问自己:“这代码写得爽吗?”

你可能会觉得 cart.sendReceipt() 不该和支付相关的方法放在一起。也许 customerNotifications.sendReceipt() 更适合存放客户消息逻辑。如果 CartHelper 的重要性足够高,它可以作为一个 控制器(controller),在内部调用所有必要的 服务(services),比如 customerNotifications

如何判断你的改动是否有益?

如果在消除临时参数的同时,代码的可读性没有受损,那么恭喜你! 你很可能构建了一个职责清晰且稳固的模块。

那么,问题来了:我们该在哪里添加新功能?

根据我的经验,在添加功能时主要有两种策略可供评估:

  1. 扩展/重构现有方法。(当新代码与现有代码逻辑足够接近时。)
  2. 在链条的所需位置创建一个新的(第 5 个)函数。(假设新代码与现有函数无关。)

最终,这会让决定新功能的归属变得更容易。(例如:cart.applyDiscounts()cart.applyTaxes()rewards.getBalance()。)

总结

在复杂的流水线中传递状态确实很棘手。然而,通过一些重构练习,你会发现自己写出的代码可读性更高,认知负荷更低。

有问题?建议?或顾虑?欢迎通过 @justsml邮件 联系我。

敬请期待本系列的下一部分

我们将探讨如何外部化状态,以及如何扩展模块的功能!

相关阅读