如何寫出好的 JavaScript —— 淺談 API 設計

發表於2017-01-04

這是 奇舞前端特訓營 JavaScript 培訓課程 的節選。很多同學覺得寫 JavaScript 很簡單,只要能寫出功能來,效果能實現就好。還有一些培訓機構,專門教人寫各種“炫酷特效”,以此讓許多人覺得這些培訓很“牛逼”。然而事實上,能寫 JavaScript 和寫好 JavaScript 這中間還有很遙遠的距離。成為專業前端,註定在 JavaScript 路途上需要一步步紮實的修煉,沒有捷徑。

t0111abe7db051d82e7

看一個簡單的例子:

實現一個類似於“交通燈”的效果,讓三個不同顏色的圓點每隔 2 秒迴圈切換。

對應的 HTML 和 CSS 如下:

那麼這一功能的 JavaScript 該如何實現呢?

版本一

有的同學說,這個實現還不簡單嘛?直接用幾個定時器一下切換不就好了:

沒錯,就這個功能本身,這樣實現就 OK 了。但是這樣實現有什麼問題呢?

首先是過程耦合,狀態切換是wait->stop->pass 迴圈,在上面的設計裡,實際上操作順序是耦合在一起的,要先 ‘wait’,然後等待 2000 毫秒再 ‘stop’,然後再等待 2000 毫秒在 ‘pass’,這中間的順序一旦有調整,需求有變化,程式碼都需要修改。

其次,這樣的非同步巢狀是會產生 callback hell 的,如果需求不是三盞燈,而是五盞燈、十盞燈,程式碼的巢狀結構就很深,看起來就很難看了。

所以我們說,版本一方法雖然直接,但因為抽象程度很低(幾乎沒有提供任何抽象 API),它的擴充套件性很不好,因為非同步問題沒處理,程式碼結構也很不好。如果只能寫這樣的程式碼,是不能說就寫好了 JavaScript 的。

版本二

要解決版本一的過程耦合問題,最簡單的思路是將狀態['wait','stop','pass']抽象出來:

這是一種資料抽象的思路,應用它我們得到了上面的這個版本。

這一版本比前一版本要好很多,但是它也有問題,最大的問題就是封裝性很差,它把 stateList 和 currentStateIndex 都暴露出來了,而且以全域性變數的形式,這麼做很不好,需要優化。

版本三

版本三是中規中矩的一版,也是一般我們在工作中比較常用的思路。應該將暴露出來的 API 暴露出來(本例中的 stateList)。將不應該暴露出來的資料或狀態隱藏(本例中的 currentStateIndex)。

有許多同學覺得說寫出這一版本來已經很不錯的。的確,應該也還不錯,但這一版的抽象程度其實也不是很高,或者說,如果考慮適用性,這版已經很好了,但是如果考慮可複用性的話,這版依然有改進空間。

我們再看一個思路上較有意思的版本。

版本四

這一版用的是過程抽象的思路,而過程抽象,是函數語言程式設計的基礎。在這裡,我們抽象出了一個 poll(...fnList) 的高階組合函式,它將一個函式列表組合起來,每次呼叫時依次輪流執行列表裡的函式。

我們說,程式設計的本質是抽象,而過程抽象是一種與資料抽象對應的思路,它們是兩種不同的抽象模型。資料抽象比較基礎,而過程抽象相對高階一些,也更靈活一些。資料抽象是研究函式如何運算元據,而過程抽象則在此基礎上研究函式如何操作函式。所以說如果把抽象比作數學,那麼資料抽象是初等數學,過程抽象則是高等數學。同一個問題,既可以用初等數學來解決,又可以用高等數學來解決。用什麼方法解決,取決於問題的模型和難度等等。


好了,上面我們有了四個版本,那麼是否考慮了這些版本就足夠了呢?

並不是。因為需求是會變更的。假設現在需求變化了:

需求變更:讓 wait、stop、pass 狀態的持續時長不相等,分別改成 1秒、2秒、3秒。

t018c41be76ca3460a2

那麼,我們發現 ——

除了版本一之外,版本二、三、四全都跪了……

t01b151b19a590ffa89

那是否意味著我們要迴歸到版本一呢?

當然並不是。


版本五

版本五的思路是,既然我們需要考慮不同的持續時間,那麼我們需要將等待時間抽象出來

這一版本里我們用了 Promise 來處理回撥問題,當然對 ES6 之前的版本,可以用 shim 或 polyfill、第三方庫,也可以選擇不用 Promise。

版本五抽象出的 wait 方法也還比較通用,可以用在其他地方。這是版本五好的一點。

版本六

我們還可以進一步抽象,設計出版本六,或者類似的物件模型

這一版本里,我們設計了一個 TrafficProtocol 類,它有 putState、reset、start 三個方法:

  • putState 接受一個函式作為引數,這個函式自身有兩個引數,一個是 subject,是由 TrafficProtocol 物件初始化時設定的 DOM 元素,一個是 next,是一個函式,表示結束當前 state,進入下一個 state。
  • reset 結束當前狀態迴圈,開始新的迴圈。
  • start 開始執行迴圈,這裡的實現是直接呼叫 reset。

看一下 reset 的實現思路:

在這裡我們建立一個 statePromise,然後將 stateList 中的方法(通過 putState 新增的)依次繫結到 promise 上。如果設定了 autoReset,那麼我們在 promise 的最後繫結 reset 自身,這樣就實現了迴圈切換。

有了這個模型,我們要新增新的狀態,只需要通過 putState 新增一個新的狀態就好了。這一模型不僅僅可以用在這個需求裡,還可以用在任何需要順序執行非同步請求的地方。

最後,我們看到,版本六用到了物件導向、過程抽象、Promise等模式,它的優點是 API 設計靈活,通用性和擴充套件性好。但是版本六也有缺點,它的實現複雜度比前面的幾個版本都高,我們在做這樣的設計時,也需要考慮是否有過度設計的嫌疑。

總結

  • 設計是把雙刃劍,繁簡需要權衡,尺度需要把握。
  • 寫程式碼簡單,程式設計不易,需要走心。

相關文章