用JavaScript的管道方法简化代码复杂性
在现代 web 开发中,维护干净有效的代码是必不可少的。随着项目的增加,我们功能的复杂性也在增加。然而,javaScript为我们提供了一个强大的工具,可以将这些复杂的函数分解为更小的、可管理的片段。在本文中,我们将探索使用 pipe 方法并通过一个真实的场景展示它的好处。
这是一个基本的pipe:
让我们从需要解决的问题开始
我们要计算用户购买各种产品时的最终价格。首先,有些产品有折扣的条件,所以我们想使用折扣对象,以便对产品的原价进行折扣计算。然后,我们要计算折扣价格的总和。如果顾客有优惠券,我们也需要考虑到这一点。最后,在以一种货币计算价格之后,将其转换成另一种货币,我们将最终价格交付给客户。
数据:
- userPurchases:代表用户购买的产品清单。每件物品都有一个唯一的标识符(id)、产品名称(name)、价格 (price)、及价格的货币(currency)。
- DISCOUNT_MAP:它将产品标识与折扣百分比联系起来。
- COUPON_USD:客户持有的美元折扣券价值,可从总购货价格中减去。
- USD_TO_EUR:美元兑换成欧元的兑换率保持不变。
const userPurchases = [
{
id: 101,
price: 34.99,
name: 'Wireless Headphones',
currency: 'USD',
},
{
id: 202,
price: 149.95,
name: 'Digital Camera Kit',
currency: 'USD',
},
{
id: 303,
price: 19.99,
name: 'Home Gym Equipment',
currency: 'USD',
},
];
const DISCOUNT_MAP = new Map([
[101, 10],
[303, 20],
[404, 30],
]);
const COUPON_USD = 75;
const USD_TO_EUR = 0.94;
在我们的设想中,将执行以下步骤:
- 根据折扣对象计算新价格。
- 计算产品的总和。
- 减去优惠券的。
- 将结果从美元改为欧元。
以下是初步实施情况:
const calculateFinalPrice = (
userPurchases,
discountMap,
userCoupon,
conversionRate,
) => {
//定义一个计算项目折扣价格的函数。
const calculateDiscount = (price, discount = 0) => {
if (discount < 0 || discount > 100) return price;
if (price < 0) return NaN;
return price * ((100 - discount) / 100);
};
//对使用折扣的物品实行折扣。
const itemsWithDiscount = userPurchases.map(item => ({
...item,
price: calculateDiscount(item.price, discountMap.get(item.id)),
}));
//计算所有项目的价格总额。
const total = itemsWithDiscount.reduce((acc, item) => acc + item.price, 0);
//从总数中减去用户的优惠券价值。
const totalAfterCoupon = total - userCoupon;
//转换成一种特定的货币。
const finalPrice = totalAfterCoupon * conversionRate;
return finalPrice;
};
如果我们仔细看代码,就会发现我们通过以下步骤解决了这个问题:
这种方法有什么不好的?
我们只在一个函数中就解决了所有这些步骤。如果我们在这4个步骤中出现了一个 bug 呢?我们就必须找出我们的函数哪一部分导致了这个问题,这个过程中我们就可能需要花费大量时间去排查问题。
所以,下面让我们看看如何实现 pipe 方法来解决这个问题。
pipe 方法
pipe 方法允许我们将一个大的、复杂的函数分解为更小的、可组合的函数。
这是一个简单实现这个函数的例子:
const pipe = (...fns) => (arg) => fns.reduce((v, fn) => fn(v), arg);
这个想法很简单,将多个函数组合起来,从左到右依次应用它们,使用前一个函数的输出作为下一个函数的输入。
例如,如果我们有函数A、B和C,并且希望它们按照 A ->B -> C 流程应用于某些数据x,我们可以使用像这样使用 pipe:
const result = pipe(
funcA,
funcB,
funcC
)(x);
以下是它的工作原理:
- pipe 以一个或多个功能作为输入,…fns 部分意味着我们可以提供任意数量的函数,用逗号分隔,它们将被视为一个列表。
- 它返回一个新的函数,可以接受某些输入数据,用 arg 表示。这个输入数据可以是任何东西,如数字、文本或更复杂的信息。
- 在这个新函数中,它使用了reduce 将列表中的每个函数应用到输入数据中,一个接一个。把它看作是通过一系列的处理步骤传递输入。
- reduce 函数负责逐步处理。从 arg 这个原始输入开始,并将列表中的第一个函数应用到列表中。然后,它获取结果并应用下一个函数,以此类推,直到它完成列表中的所有函数。
- 最后,它返回将所有这些函数应用于输入数据的最终结果。
现在,让我们应用这些概念:
//定义一个计算项目折扣价格的函数。
const calculateDiscount = (price, discount = 0) => {
if (discount < 0 || discount > 100) return price;
if (price < 0) return NaN;
return price * ((100 - discount) / 100);
}
// 创建一个函数使用discountMap
const applyDiscounts = discountMap => items =>
items.map((item) => ({
...item,
price: calculateDiscount(item.price, discountMap.get(item.id)),
}));
// 定义一个计算所有项目价格合计的函数。
const calculateTotal = items =>
items.reduce((total, item) => total + item.price, 0);
//创建一个函数,从中减去用户的优惠券价值。
const subtractCoupon = coupon => total => total - coupon;
//转换成一种特定的货币。
const convertTo = conversionRate => total => total * conversionRate;
const calculateFinalPrice = (items, discountMap, coupon, conversionRate) =>
pipe(
applyDiscounts(discountMap),
calculateTotal,
subtractCoupon(coupon),
convertTo(conversionRate),
)(items);
我们从用户购买的数据开始,在每个步骤中,我们应用一个特定的函数并转换数据,直到我们得到最终结果。
就像这样:
这种办法的好处是什么?
我们已经创建了几个较小的、专门的函数来解决一个具体的问题。我们现在可以在应用程序的其他地方使用它们。我们可以分别测试每个函数。
例如,如果我们想测试折扣计算功能,我们可以这样做:
import { calculateDiscount } from './calculateDiscount';
describe('calculateDiscount function', () => {
it('当折扣为负数时,应退回原价。', () => {
const price = 100;
const discount = -10;
expect(calculateDiscount(price, discount)).toBe(100);
});
it('当折扣为零时,应退回原价。', () => {
const price = 100;
const discount = 0;
expect(calculateDiscount(price, discount)).toBe(100);
});
});
结论
通过将复杂的流程分解为较小的可测试单元,我们可以提高代码质量,减少错误,并使我们的代码库更容易理解。