談談Bug引起的複雜性“Bug-O” — Overreacted

banq發表於2019-01-27

在編寫對效能敏感的程式碼時,最好記住它的演算法複雜性。它通常用Big-O表示法表示
Big-O衡量程式碼在向其投入更多資料時會變慢多少。例如,如果排序演算法具有O(n 2)複雜度,則排序×50倍以上的專案將大約為50 2 =慢2,500倍。Big O不會給你一個確切的數字,但它可以幫助你理解演算法如何擴充套件。
一些例子:O(n),O(n log n),O(n 2),O(n!)。
但是,這篇文章與演算法或效能無關。它是關於API和除錯的。事實證明,API設計涉及非常類似的考慮因素。
我們的大部分時間都用於查詢和修復程式碼中的錯誤。大多數開發人員希望更快地發現錯誤。儘管最終可能令人滿意,但是如果您已經實施了路線圖中的某些內容,那麼花費一整天時間來追逐單個錯誤是很糟糕的。
除錯經驗會影響我們對抽象,庫和工具的選擇。一些API和語言設計使得錯誤變得不可能。有些則會產生無窮無盡錯誤,如何分辨?

我有一個指標可以幫助我思考這個問題。我把它稱為Bug-O表示法。

Big-O描述了隨著輸入增長,演算法減慢了多少。Bug-O描述了隨著你的程式碼的增長,API讓你減緩多少。
例如,考慮一下這個程式碼,它會隨著時間的推移使用node.appendChild(),node.removeChild()手動更新DOM,且這兩個函式沒有明確的結構:

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();
    });
  })
}

這段程式碼的問題並不在於它“醜陋”。我們不是在談論美學。問題是,如果此程式碼中存在錯誤,我不知道從哪裡開始查詢。
根據回撥和事件觸發的順序,該程式可能採用的程式碼路徑數量會出現組合爆炸。在其中一些中,我會看到正確的資訊。在其他情況下,我會看到多個微調器,故障和錯誤訊息,並可能崩潰。
此功能有4個不同的部分,不保證其順序。我的非科學計算告訴我,他們可以執行4×3×2×1 = 24個不同的順序。如果我再新增四個程式碼段,它將是8×7×6×5×4×3×2×1 - 四萬組合。祝你好運除錯。

換句話說,這種方法的Bug-O是BUg(n!),其中n是觸及DOM的程式碼段的數量。是的,這是一個因素。當然,我在這裡並不是很科學。在實踐中並非所有轉換都是可能的。但另一方面,這些細分中的每一個都可以執行多次。

為了改進此程式碼的Bug-O,我們可以限制可能的狀態和結果的數量。我們不需要任何庫來執行此操作。這只是在我們的程式碼上強制執行某些結構的問題。這是我們可以做到的一種方式:

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;
  }
}

此程式碼可能看起來不太相似。它甚至有點冗長。但由於這個思路,除錯起來非常簡單:

function setState(nextState) {
  // Clear all existing children
  formStatus.innerHTML = '';

  // ... the code adding stuff to formStatus ...



透過在執行任何操作之前清除表單狀態,我們確保我們的DOM操作始終從頭開始。這就是我們如何對抗不可避免的 - 不要讓錯誤累積起來。這是相當於“關閉再開啟”的編碼,它的效果非常好。

如果在輸出中出現錯誤,我們只需要考慮一個退一步-以前的setState電話。除錯渲染結果的Bug-O是Bug(n),其中n是渲染程式碼路徑的數量。這裡只有四個(因為我們在a中有四個案例switch)。

我們在設定狀態時可能仍然存在競爭條件,但除錯它們更容易,因為可以記錄和檢查每個中間狀態。我們還可以明確禁止任何不需要的轉換:
當然,總是重置DOM需要權衡。每次都過分刪除和重新建立DOM會破壞其內部狀態,失去焦點,並在較大的應用程式中導致可怕的效能問題。
這就是像React這樣的庫包可以提供幫助的原因。它們讓您在總是從頭開始重新建立UI的範例中思考,而不必這樣做:

function FormStatus() {
  let [state, setState] = useState({
    step: 'initial'
  });

  function handleSubmit(e) {
    e.preventDefault();
    if (state.step === 'pending') {
      // Don't allow to submit twice
      return;
    }
    setState({ step: 'pending' });
    submitForm.then(() => {
      setState({ step: 'success' });
    }).catch(error => {
      setState({ step: 'error', error });
    });
  }

  let content;
  switch (state.step) {
    case 'pending':
      content = <Spinner />;
      break;
    case 'success':
      content = <SuccessMessage />;
      break;
    case 'error':
      content = (
        <>
          <ErrorMessage error={state.error} />
          <RetryButton onClick={handleSubmit} />
        </>
      );
      break;
  }

  return (
    <form onSubmit={handleSubmit}>
      {content}
    </form>
  );
}

程式碼可能看起來不同,但原理是相同的。元件抽象強制執行邊界,以便您知道頁面上沒有其他程式碼可以混淆其DOM或狀態。元件化有助於減少Bug-O。
 

相關文章