“Bug-O” 符號
當你編寫對效能要求高的程式碼時,考慮演算法複雜度是個好辦法,用Big-O 符號 表示。
Big-O 用來衡量投入更多資料時程式碼會慢多少 。例如,如果有個排序演算法的複雜度是 O( n2 ),排序50倍以上的資料大概要慢 502 = 2,500 時間。Big O 不會給出一個準確的數值,但它可以幫助你知道演算法效果 如何。
一些例子:O(n ), O(n logn ), O( n2 ), O(n! )。
然而,這篇文章與演算法或效能無關,與APIs和除錯有關 。 事實證明,API設計涉及到十分相似的考慮事項。
我們大部分時間都用於查詢和修復程式碼中的錯誤,大部分開發者希望可以更快的找到bugs。儘管最後的結果可能讓人滿意,但當你已經制定好工作流程時,花費一整天時間來找一個bug是很糟糕的。
除錯經驗會影響我們對抽象、類庫和工具的選擇。一些 API 和語言設計可以杜絕某類錯誤,一些則會引發無數個錯誤,可是我們怎麼知道要選擇哪個呢 ?
許多APIs的線上討論主要是關於美學上的,但其中沒有太多提到實際使用後的感受。
我有一個指標可以幫助我思考這個問題,我稱它為 Bug-O符號:
:beetle:(n )
Big-O 描繪的是演算法隨著輸入增長會變慢多少,Bug-O 描繪的是隨著程式碼增長會變慢多少。
例如,請思考下面程式碼,隨著時間流逝,使用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(); }); }) } 複製程式碼
程式碼的問題不在於它 “醜”,我們不討論美學,問題在於如果在程式碼中存在一個bug,我不知道要從哪裡開始找 。
順序由回撥和事件觸發決定,這個程式的程式碼路徑數量可以引發組合爆炸。可能最後我會看到正確的提示,也可能我會看到多個 spinners、失敗和錯誤提示同時出現或者程式碼崩潰。
這個方法有4個部分且無法保證它們的執行順序,我用非常不科學的方法計算,結果告訴我會有 4×3×2×1 = 24 種執行順序。如果我新增更多程式碼塊,就可能是 8×7×6×5×4×3×2×1 ——四萬 種組合,祝你除錯順利。
就是說,這示例中,Bug-O 為:beetle:(n! ),這裡n 表示程式碼中涉及DOM的程式碼塊數量,這是個階層 。當然,這不是很科學的計算。在實際中,不可能所有的部分都可以轉換,但另一方面,每一段都可以被重複使用,這樣 :beetle:( ¯\(ツ) /¯ ) 也許能更恰當些,但仍然很差勁,我們可以做得更好。
為了改善這程式碼的 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始終從頭開始。這就是我們解決不可避免的熵 ——不 要讓錯誤累積起來。這就相當於 “關閉再開啟” 的程式碼,效果非常好。
如果輸出存在bug,我們只需要回退一
步 —— 前一次setState
呼叫。這種程式碼除錯的 Bug-O 複雜度為 :beetle:(n
),n
是render分支的數量,在這是4(因為switch
的分支是4)。
在狀態賦值 中,我們可能還需要一些條件判斷,但除錯起來還是挺容易的,因為可以記錄和檢查每個行進中的狀態值,我們也可以避免任何不想要的顯示轉換:
function trySubmit() { if (currentState.step === 'pending') { // Don't allow to submit twice return; } 複製程式碼
當然,總是重置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或state。元件化有助於減小Bug-O。
實際上,如果 React App 中任何 值看起來有問題,你可以通過在React樹逐個檢視元件上的程式碼跟蹤它的來源。不管 app 有多大,跟蹤的值都等於 Bug-O:beetle:(樹高 )。
你下次看見API討論時,請考慮:常見的除錯任務的 :beetle:(n ) 是多少?你當前熟悉的APIs和原則怎麼樣?Redux、CSS、繼承 —— 它們都有自己的 Bug-O。
翻譯原文The “Bug-O” Notation(2019-01-25)