
观点:每个业务流程本质都是数据管道,代码复杂常因掩盖了管道结构
来源标题:Your Program Is a Data Pipeline (Even If You Don't See It)
文章提出,复杂问题的最佳解法往往显得理所当然,这种“必然感”来自写代码前先想清问题的形状。其核心观点是:每个业务流程天然就是一条数据管道——数据流入、被转换、再流出;代码难以理解通常不是业务逻辑本身复杂,而是管道被临时检查和分散逻辑层层掩盖。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
“糟糕的程序员担心代码。好的程序员担心数据结构和它们之间的关系。”——Linus Torvalds
多年来我学到的一件事是,复杂问题的最佳解决方案看起来并不复杂。它们看起来显而易见。你读代码时会想“嗯,当然了,还能怎么做?”——这种必然感并非偶然。它是某人在写第一行代码之前花时间思考问题形态的结果。
我职业生涯的大部分时间都在追逐这种感觉。并不总是成功——我多次把事情过度复杂化,并从中痛苦地学到了“优雅”和“简单”不是一回事。但一路走来,我开发了一个思维工具,帮助我稳定地达到那个状态。它不是框架,也不是设计模式。它是一种看待问题的方式,能防止意外的复杂性悄然渗入。核心思想是:每个业务流程天然就是一条管道。数据进来,被转换,再出去。当代码变得难以理解时,几乎从来不是因为业务逻辑本身复杂——而是因为我们把管道埋藏在了一层层的临时检查和散落的逻辑之下。
这篇文章是我试图解释我的做法。首先,是向我自己解释。多年来我一直在直觉性地应用它,但从未尝试把它付诸文字。那么,开始吧。
看看这段代码。我打赌你见过类似的东西:
function calculateTotal(cart, user, promoCode) { let total = cart.subtotal; let appliedDiscounts = []; if (cart.items.some(i => i.quantity >= 3 && i.eligibleForB2G1)) { const cheapest = findCheapestEligible(cart.items); total -= cheapest.price; appliedDiscounts.push("buy2get1"); if (user.tier === 'gold' && !appliedDiscounts.includes("SUMMER20")) { if (!cart.items.every(i => i.excludedFromLoyalty)) { total -= total * 0.10; appliedDiscounts.push("loyalty"); if (promoCode && isValidPromo(promoCode)) { const promo = getPromo(promoCode); if (promo.exclusive && appliedDiscounts.length > 0) { // can't apply } else if (!promo.exclusive || appliedDiscounts.length === 0) { total -= promo.calculate(total); appliedDiscounts.push(promoCode); let shippingCost = total > 50 ? 0 : (getWeight(cart) > 10 ? 8.99 : 4.99); if (user.subscription === 'prime') shippingCost = 0; let tax = calculateTax(total + shippingCost, cart.destination); return total + shippingCost + tax; }
现在眯起眼睛看。忘掉那些 if 语句。这个函数到底在做什么?
它接收一个购物车、一个用户和一个促销码。它计算折扣。然后是运费。然后是税费。然后返回一个数字。就这样。它是一条管道:输入 → 折扣 → 运费 → 税费 → 输出。业务逻辑很直白。代码却不直白。
而且情况更糟。在代码库的某个地方,收据渲染器需要显示“你省了 X”,但这个信息在这个函数内部计算完就立刻丢失了。邮件服务需要预计送达日期——这里根本没算。运费逻辑重新检查了用户等级,而这个等级在折扣计算时已经检查过了。税费计算依赖于一个 total 变量,而它在到达那里之前已经被悄悄修改了三次。
管道一直都在那里。我们只是把它埋了。
这里的本能反应是重构。提取方法。也许把它包进一个类里:
class OrderCalculator { private total: number; private appliedDiscounts: string[] = []; constructor(private cart, private user, private promoCode) { this.total = cart.subtotal; calculate() { this.applyBuy2Get1(); this.applyLoyaltyDiscount(); this.applyPromoCode(); const shipping = this.calculateShipping(); const tax = this.calculateTax(shipping); return this.total + shipping + tax; private applyBuy2Get1() { /* mutates this.total, pushes to this.appliedDiscounts / } private applyLoyaltyDiscount() { / checks this.appliedDiscounts, mutates this.total / } private applyPromoCode() { / checks this.appliedDiscounts, mutates this.total / } private calculateShipping() { / reads this.total, reads this.user / } private calculateTax(shipping) { / reads this.total */ } }
看起来更干净了,对吧?calculate() 方法甚至读起来就像一条管道。但问题是一样的:
我们给混乱起了更好的名字。我们没有修复流程。管道仍然被隐藏着——只是藏在了方法边界后面,而不是一个大函数内部。
那么数据优先的方法有什么不同?我不再问“有哪些步骤?”,而是问一个不同的问题:“每个消费者需要什么,什么样的数据形态能让获取这些数据变得轻而易举?”
收据需要一份已应用折扣及其金额的列表。邮件需要预计送达日期。支付处理器需要最终总额。结账页面需要所有配送选项及其价格。这些消费者都不应该自己去推导任何东西——它们应该直接读取字段。
所以我倒着来。什么样的数据结构能让结账页面渲染起来轻而易举?一个已经包含 shippingOptions、appliedDiscounts、totalSaved、finalPrice——全部预先计算好的结构。什么能让它容易产出?一个已经知道折后总额的已解析折扣结果。什么能让那变得容易?把规则定义为数据,而不是嵌套的 if 块。
const discountRules = [ { id: "buy2get1", priority: 1, stackable: true, exclusive: false, condition: (cart, user) => cart.items.some(i => i.quantity >= 3 && i.eligible), apply: (cart) => ({ saved: findCheapestEligible(cart).price }) }, { id: "loyalty", priority: 2, stackable: true, exclusive: false, condition: (cart, user) => user.tier === 'gold', apply: (cart, runningTotal) => ({ saved: runningTotal * 0.10 }) }, { id: "SUMMER20", priority: 3, stackable: false, exclusive: true, condition: (cart, user, promo) => promo === "SUMMER20", apply: (cart, runningTotal) => ({ saved: runningTotal * 0.20 }) }, ];
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。