让我们继续有关代码重构的一系列简短帖子!在其中,我们讨论了可以帮助您改善代码和项目的技术和工具。
今天,我们将讨论纯粹功能和参照透明度的好处。
效果问题
应用程序与现实世界之间的任何相互作用都是副作用。
当我们将数据存储在DB中或将其渲染在屏幕上时,我们会产生副作用。我们无法创建一个没有副作用的有用应用。
另一方面,副作用的主要问题是它们是不可预测的。他们改变了状态,因此我们不能确定代码的结果始终是相同的。
不受控制的副作用使危险和脆弱的代码。因此,我们需要与它们合作的策略。
纯函数
简而言之,pure functions是具有2个属性的
- 他们不产生副作用
- 并在使用相同参数调用时始终返回相同的结果。
纯函数使代码可预测和可重现。反过来,它消除了这种脆弱性的感觉,因为如果我们可以再现 一个错误,我们可以很快隔离并修复它。
让我们看看一个例子。在函数prepareExport
中,我们计算产品列表中每个项目的总价格和最新运输日期:
function prepareExport(items) {
let latestShipmentDate = 0;
for (const item of items) {
item.subtotal = item.price * item.count;
if (item.shipmentDate >= latestShipmentDate) {
latestShipmentDate = item.shipmentDate;
}
}
for (const item of items) {
item.shipmentDate = latestShipmentDate;
}
return items;
}
函数中的subtotal
计算更改items
阵列,这是副作用。
但是,请注意其他计算也取决于此数组,并且更改它也会影响它们。这意味着,在计算subtotal
时,我们必须考虑如何影响shipmentDate
计算。
功能越广泛,会影响越多的动作,我们必须记住的细节越多。详细信息越多,我们的working memory占代码占据的范围越多。
编写更改(即效果)很难遵循,请记住并与之合作。让我们尝试重写和改进功能。
改进代码
我们可以避免效果,而不是跟踪效果。让我们重写代码不要更改共享状态,而是将问题表示为一系列步骤。
function prepareExport(items) {
// 1. Calculate the subtotals.
// The result is a new array.
const withSubtotals = items.map((item) => ({
...item,
subtotal: item.price * item.count,
}));
// 2. Calculate the shipment date.
// The result of the previous step is the input.
let latestShipmentDate = 0;
for (const item of withSubtotals) {
if (item.shipmentDate >= latestShipmentDate) {
latestShipmentDate = item.shipmentDate;
}
}
// 3. Append the date to each position.
// The result is yet another new array.
const withShipment = items.map((item) => ({
...item,
shipmentDate: latestShipmentDate,
}));
// 4. Return the result of step 3,
// as the result of the function.
return withShipment;
}
我们还可以将步骤提取到单独的功能中,并在必要时分别重构:
function calculateSubtotals(items) {
return items.map((item) => ({ ...item, subtotal: item.price * item.count }));
}
function calculateLatestShipment(items) {
const latestDate = Math.max(...items.map((item) => item.shipmentDate));
return items.map((item) => ({ ...item, shipmentDate: latestDate }));
}
然后,prepareExport
函数看起来像是数据转换序列的结果:
function prepareExport(items) {
const withSubtotals = calculateSubtotals(items);
const withShipment = calculateLatestShipment(withSubtotals);
return withShipment;
}
// items
// -> itemsWithSubtotals
// -> itemsWithSubtotalsAndShipment
,甚至喜欢这样,如果我们使用Hack Pipe Operator,在撰写本文时,它在阶段2:
const prepareExport =
items |> calculateSubtotals(%) |> calculateLatestShipment(%);
该代码的这种布置称为函数管道。它可以帮助我们以与现实世界相关联的方式表示代码。
我们,有点描述了问题,好像我们在将其描述给另一个人一样:
首先,我们做一个;然后,我们制作B;最后,我们有C作为结果。
这样的描述可帮助我们在修改应用程序或在代码库中搜索错误时搜索所需的代码。
参考透明度
生成的代码不仅易于阅读,而且更容易测试和搜索其中的错误。
纯函数的力量是它们是可重复的。这意味着当我们将相同的参数传递给纯函数时,它将始终返回相同的结果。
因此,如果我们有一系列纯函数,我们可以在任何时候将其拆分,并用其结果替换所有以前的调用,并且整体功能不会改变:
1. If we have a sequence of pure transformations
from 🍇 to 🍌:
🍇 → 🍏 → 🍒 → 🍊 → 🍌
2. Then we can remove all the calls before 🍊
and replace them with only their result,
and the result (🍌) won't change:
🍒 → 🍊 → 🍌
此属性称为referential transparency,它有助于使代码更容易调试和测试,因为我们可以在任何时候切碎序列,并使用我们想要的任何数据运行其余功能:< br>
// Let's say we're searching for a bug
// in the `prepareExport` function.
function prepareExport(items) {
const withSubtotals = calculateSubtotals(items);
const withShipment = calculateLatestShipment(withSubtotals);
return withShipment;
}
// If we don't yet know where exactly the problem is,
// we can test each of the substeps in isolation:
expect(calculateSubtotals(items)).toEqual(expectedTotals);
// And if we know that the first step is fine,
// we can “chop” the function and “start” it
// from a particular point with the specific data:
function prepareExport(items) {
// Comment this out:
// const withSubtotals = ...
// “Feed” the next step with particular data
// and “start” the function from this point:
const withShipment = calculateLatestShipment(specificData);
return withShipment;
}
prepareExport
内部的步骤仅通过其输入和输出数据连接。他们没有可以影响其运营的共同状态。该函数成为数据转换链,每个函数都与其他数据隔离,不能从外部受到影响。
有关我书中重构的更多信息
在这篇文章中,我们仅讨论了纯函数的一个方面及其对重构代码的好处。
我们尚未提及它们如何帮助修复模块的抽象和封装或如何改善代码可检验性。
如果您想进一步了解这些方面并一般重构,我鼓励您查看我的在线书:
这本书是免费的,可以在Github上获得。在其中,我更详细地解释了这个主题。
希望您发现它有帮助!享受这本书ð