让我们继续有关代码重构的一系列简短帖子!在其中,我们讨论了可以帮助您改善代码和项目的技术和工具。
今天,我们将讨论使用早期回报技术简化复杂条件。例如,我们将使用React组件并改善其渲染函数。
组件代码
在我们进行重构之前,让我们研究要使用的代码。
在这里,我们有一个Checkout
组件,该组件为特定用户提供购物车,并渲染结帐表,成功结帐消息,错误或加载状态指标:
function Cart({ user }) {
// Imagine, the `useCart` hook is responsible
// for fetching the cart by the user id,
// handling the fetching status and errors,
// and handling the submit event:
const { cart, status, error, onSubmit } = useCart(user.id);
// Assume that the status can be in 4 states:
// error, idle, loading, and submitted.
const hasError = status === "error";
const isIdle = status === "idle";
const isLoading = status === "loading";
// The render condition is complicated
// and difficult to read and reason about:
if (!hasError) {
if (isIdle) {
return (
<form onSubmit={onSubmit}>
<ProductList products={cart.products} />
<button>Purchase</button>
</form>
);
} else if (isLoading) {
return "Loading...";
}
if (!isLoading && !isIdle) {
return "We'll call you to confirm the order.";
}
} else {
return error;
}
}
让我们假设获取数据,提交表单和更新状态的主要逻辑被封装在useCart
钩中,并且该组件仅负责UI的渲染。
考虑到这一点,我们可以分析代码并查看我们可以在其中找到哪些问题。
代码复杂性
组件渲染函数的主要问题是其复杂性。但是它仅需17行代码,所以为什么我们将其视为复杂?
要回答,让我们注意此代码中嵌套的水平:
function Checkout() {
__// ...
__if (!hasError) {
____if (isIdle) {
______return (...)
____} else if (isLoading) {
______return "Loading...";
____}
____
____if (!isLoading && !isIdle) {
______return "We'll call you to confirm the order.";
____}
__} else {
____return error;
__}
}
文本左侧的空白空间显示了摘要包含多少信息。
信息越多,使用代码时需要记住的详细信息越多。当我们阅读它时,我们的working memory被大量细节所占据,很难跟踪代码中发生的事情。因此,当我们看到深度嵌套的代码时,我们已经知道很难理解。
实际上有一个代码指标来描述这种现象称为cognitive complexity。
那么我们如何使代码更好?
使代码更好
当我们看到复杂的条件时,我们可以应用各种不同的技术使它们变得更简单。这些技术之一是提早回报。
在这种情况下,我们的主要目标是使条件在感知上更简单,同时保持其含义相同。我们可以通过使条件 flatter 。
来实现这一目标。正如我们前面提到的,该代码的主要问题是我们需要同时牢记的大量细节。但是请注意,某些条件分支包含边缘案例:
function Checkout({ user }) {
// ...
if (!hasError) {
if (isIdle) {
// Happy Path.
} else if (isLoading) {
// Edge case for the “Loading” state.
}
if (!isLoading && !isIdle) {
// Edge case for the “Submitted” state.
}
} else {
// Edge case for the “Error” state.
}
}
如果我们将条件转到内部并首先处理这些分支怎么办?
function Checkout({ user }) {
// ...
// Handle the “Error” state edge case first:
if (hasError) return error;
// Then, handle everything else:
if (isIdle) {
// ...
} else if (isLoading) {
// ...
}
if (!isLoading && !isIdle) {
// ...
}
}
嵌套的水平已经下降,并且状况已经 更简单。这里的关键词似乎是因为实际功能保持不变。我们只更改了执行顺序以及我们必须记住多少信息。
让我们继续:
function Checkout({ user }) {
// ...
if (hasError) return error;
if (!isLoading && !isIdle) return "We'll call you to confirm the order.";
if (isLoading) return "Loading...";
if (isIdle) {
// Happy Path.
}
}
再次,我们将边缘箱子分支并首先处理。这样,我们有点过滤UI声明不是闲置的状态。
使用早期退货,我们还可以注意到一些我们以前从未注意到的不必要的检查。例如,我们可以稍微重新排列订单并摆脱额外的支票:
function Checkout({ user }) {
// ...
// (Here, we use the assumption
// that `status` can be only in 4 states:
// loading | idle | error | submitted.
// So in the condition we now:
// - first, we check for `error`,
// - then, for `loading`,
// - then, for `submitted`,
// - and finally, we're left only with `idle`.
if (hasError) return error;
if (isLoading) return "Loading...";
if (!isIdle) return "We'll call you to confirm the order.";
// Happy Path.
}
病情现在变得平坦。如果我们遇到了其中一个,它将一一检查边缘案例,然后返回。如果该函数继续工作,则意味着 没有发生任何边缘情况。
过滤边缘案例使我们能够忘记检查的分支。他们不再占据我们的工作记忆,并且从感知中,条件变得更加简单。
比较结果
好吧,让我们编译最终的重构结果并比较代码。这是初始代码:
function Checkout({ user }) {
const { cart, status, error, onSubmit } = useCart(user.id);
const hasError = status === "error";
const isIdle = status === "idle";
const isLoading = status === "loading";
if (hasError) {
if (isIdle) {
return (
<form onSubmit={onSubmit}>
<ProductList products={cart.products} />
<button>Purchase</button>
</form>
);
} else if (isLoading) {
return "Loading...";
}
if (!isLoading && !isIdle) {
return "We'll call you to confirm the order.";
}
} else {
return error;
}
}
这是重构版本:
function Checkout({ user }) {
const { cart, status, error, onSubmit } = useCart(user.id);
const hasError = status === "error";
const isLoading = status === "loading";
const isSubmitted = state === "submitted";
if (hasError) return error;
if (isLoading) return "Loading...";
if (isSubmitted) return "We'll call you to confirm the order.";
return (
<form onSubmit={onSubmit}>
<ProductList products={cart.products} />
<button>Purchase</button>
</form>
);
}
现在,渲染函数更加简单,更少可怕。左侧的空白空间较小,因此它不会立即触发我们期望很多信息。
我们一一检查边缘案例并将其过滤掉。在功能结束时,我们只需要处理快乐的路径。
基本上,我们从这种思考状况的方式开始:
...对此:
该功能保持不变,但是现在的条件似乎更简单,更易于阅读。
早期退货不仅适用于渲染功能,而且适用于通常的任何条件。它可能不适合防御性编程:当我们明确处理条件的每个分支时。但是作为默认策略,在重构代码时可能会有所帮助。
有关我书中重构的更多信息
在这篇文章中,我们只讨论了一种可以帮助我们使条件更平坦和更可读性的技术。
我们没有提到设计模式(例如策略),布尔代数(de Morgan的定律)或模式匹配。我们也没有讨论过州机器,我认为,与有条件的组件渲染相比,它比早期返回要好得多。
如果您想进一步了解这些内容并总体上进行重构,我鼓励您查看我的在线书:
这本书是免费的,可以在Github上获得。在其中,我更详细地解释了这个主题。
希望您发现它有帮助!享受这本书ð