重构工具:提早返回较平坦的条件
#javascript #教程 #react #重构

让我们继续有关代码重构的一系列简短帖子!在其中,我们讨论了可以帮助您改善代码和项目的技术和工具。

今天,我们将讨论使用早期回报技术简化复杂条件。例如,我们将使用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>
  );
}

现在,渲染函数更加简单,更少可怕。左侧的空白空间较小,因此它不会立即触发我们期望很多信息。

我们一一检查边缘案例并将其过滤掉。在功能结束时,我们只需要处理快乐的路径。

基本上,我们从这种思考状况的方式开始:

Condition with many interdependent branches

...对此:

Condition model with filtering the edge cases first

该功能保持不变,但是现在的条件似乎更简单,更易于阅读。

早期退货不仅适用于渲染功能,而且适用于通常的任何条件。它可能不适合防御性编程:当我们明确处理条件的每个分支时。但是作为默认策略,在重构代码时可能会有所帮助。

有关我书中重构的更多信息

在这篇文章中,我们只讨论了一种可以帮助我们使条件更平坦和更可读性的技术。

我们没有提到设计模式(例如策略),布尔代数(de Morgan的定律)或模式匹配。我们也没有讨论过州机器,我认为,与有条件的组件渲染相比,它比早期返回要好得多。

如果您想进一步了解这些内容并总体上进行重构,我鼓励您查看我的在线书:

这本书是免费的,可以在Github上获得。在其中,我更详细地解释了这个主题。

希望您发现它有帮助!享受这本书ð