优秀的编程知识分享平台

网站首页 > 技术文章 正文

“Bug-O”表示法(bug的名词)

nanyue 2024-08-06 18:02:08 技术文章 5 ℃

最近看了Dan Abramov的一篇文章(https://overreacted.io/zh-hans/the-bug-o-notation/),大受启发,通过Bug-O去衡量代码的好与坏,比通过其它方式去衡量会好很多。

一、背景

随着项目越来越复杂,代码越来越多,如何写出可维护性的代码成为我们当前面临的一个难题,我们的项目经过好多次迭代,代码已经像屎一样了,虽然经过多次重构,代码已经好很多的,但是核心代码还是不敢碰,一个组件2000行3000行那是家常便事。

在这样的背景下,靠大家自觉去优化代码、写好代码是很困难的(毕竟每个人的代码风格都不一样),于是我们制定了一套强制性的代码规范,其中有几条是这样的:1)组件小于50行可以用Hooks,超过50行必须用class component;2)一个函数不能超过30行;3)禁止复制粘贴,也就是重复代码。每次CodeReview。

我们就对着这些规范来,不可否认,代码的整体复杂度是减下来了,至少更容易维护些。“但是通过组件各种解藕拆封(细分组件、自定义 hooks )后,代码就一定更方便维护了吗?怎么评估?”,这是我们当时有位同学提出来的。


二、定义

  1. Bug-O 表示法描述随着代码量的增大,这个 API 会让你做一件事变得多慢。
  2. 另外一种思路去描述代码的可读性、可维护性,而不是代码行数、美观度


三、例子

首先我们来看这篇文章提到的例子,如下:

function trySubmit() {
    // Section 1
    let spinner = createSpinner();
    formStatus.appendChild(spinner);
    submitForm().then(() => {
        // Section 2
      formStatus.removeChild(spinner);
      let successMessage = createSuccessMessage();
      formStatus.appendChild(successMessage);
    }).catch(error => {
        // Section 3
      formStatus.removeChild(spinner);
      let errorMessage = createErrorMessage(error);
      let retryButton = createRetryButton();
      formStatus.appendChild(errorMessage);
      formStatus.appendChild(retryButton)
      retryButton.addEventListener('click', function() {
        // Section 4
        formStatus.removeChild(errorMessage);
        formStatus.removeChild(retryButton);
        trySubmit();
      });
    })
  }
  

我画一个图来表示一下,图中的每个节点代表一段逻辑或一个函数,当出现BUG的时候(如图所示),我们很难定位,代码错综复杂,我们必须从上至下一层一层地排查,每个逻辑分支也要注意排查,就如Dan所说,极端情况下的复杂度是O(n!)。

这样的代码我们都知道是“烂”代码,很多重复的逻辑是可以抽离出来的,于是大部分人都会抽离公共utils(如下图所示),这是代码重构最容易的事情,也是我们团队目前所使用的方法,为了不让代码行数超过阈值,拆了很多utils,为了想用Hooks,拆了好多组件。

但是当出现BUG的时候,就一定能快速定位问题嘛,我看也未必,在实际场景中,这样会造成代码跳来跳去,通常一段逻辑是比较耦合的,但是它拆分到好几个地方,看着像是清晰了,但是代码的可读性并不是很清晰(当然啦,比上面完全没有拆分的肯定好很多)

还能继续优化么,Dan给了另外一种思路,通过状态树去组织逻辑,先看优化后的代码:

let currentState = {
    step: 'initial', // 'initial' | 'pending' | 'success' | 'error'
  };
  
  function trySubmit() {
    if (currentState.step === 'pending') {
      // Don't allow to submit twice
      return;
    }
    setState({ step: 'pending' });
    submitForm().then(() => {
      setState({ step: 'success' });
    }).catch(error => {
      setState({ step: 'error', error });
    });
  }
  
  function setState(nextState) {
    // Clear all existing children
    formStatus.innerHTML = '';
  
    currentState = nextState;
    switch (nextState.step) {
      case 'initial':
        break;
      case 'pending':
        formStatus.appendChild(spinner);
        break;
      case 'success':
        let successMessage = createSuccessMessage();
        formStatus.appendChild(successMessage);
        break;
      case 'error':
        let errorMessage = createErrorMessage(nextState.error);
        let retryButton = createRetryButton();
        formStatus.appendChild(errorMessage);
        formStatus.appendChild(retryButton);
        retryButton.addEventListener('click', trySubmit);
        break;
    }
  }

代码行数是增加了,但逻辑更清晰了,当现BUG的时候,很容易DEBUG,因为它基本没有什么逻辑分支,通过新增一个状态让各部分逻辑各司其职,这样只要根据状态就能快速定位问题,如下图所示,即使从根位置开始定位问题,那么也仅需要两层就能定位到问题,它的复杂度是O(tree height),也即是树的高度,这样的代码才更好维护,再怎么加逻辑都不怕


那怎么样才能写出这样的代码来呢,我们看到上面例子是加了一个状态,将状态与UI分离,通过状态去驱动UI,这是一种很好的思路,实际上Dan也是这样建议的,他建议用容器组件与展示组件去组织逻辑,他建议用Redux等第三方状态管理库去管理状态,都是差不多的思路。

如上图所示,我们将状态保存到Redux,借助Redux-Devtools插件,当出现BUG的时候,我们只要关心状态是否符合预期就行,甚至能在O(1)时间内定位问题。


下个阶段,我们项目将全面拥抱Redux、Redux-Toolkits来管理日益复杂的项目,以可维护性也即Bug-O”来衡量代码质量量。

(完)

Tags:

最近发表
标签列表