国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
從達標(biāo)到卓越--API設(shè)計之道

新技術(shù)層出不窮,長江后浪推前浪,而浪潮褪去后能留下來的,是一些經(jīng)典的設(shè)計思想。

在前端界,以前有遠近聞名的 jQuery,近來有聲名鵲起的 Vue.js。這兩者叫好又叫座的原因固然有很多,但是其中有一個共同特質(zhì)不可忽視,那便是它們的 API 設(shè)計 非常優(yōu)雅。

因此這次我想來談個大課題 —— API 設(shè)計之道。


討論內(nèi)容的定義域

本文并不是《jQuery API 賞析》,當(dāng)我們談?wù)?API 的設(shè)計時,不只局限于討論「某個框架應(yīng)該如何設(shè)計暴露出來的方法」。作為程序世界分治復(fù)雜邏輯的基本協(xié)作手段,廣義的 API 設(shè)計涉及到我們?nèi)粘i_發(fā)中的方方面面。

最常見的 API 暴露途徑是函數(shù)聲明(Function Signiture),以及屬性字段(Attributes);而當(dāng)我們涉及到前后端 IO 時,則需要關(guān)注通信接口的數(shù)據(jù)結(jié)構(gòu)(JSON Schema);如果還有異步的通信,那么事件(Events)或消息(Message)如何設(shè)計也是個問題;甚至,依賴一個包(Package)的時候,包名本身就是接口,你是否也曾碰到過一個奇葩的包名而吐槽半天?

總之,「API 設(shè)計」不只關(guān)乎到框架或庫的設(shè)計者,它和每個開發(fā)者息息相關(guān)。

提綱挈領(lǐng)

有一個核心問題是,我們?nèi)绾卧u判一個 API 的設(shè)計算「好」?在我看來,一言以蔽之,易用。

那「易用」又是什么呢?我的理解是,只要能夠足夠接近人類的日常語言和思維,并且不需要引發(fā)額外的大腦思考,那就是易用。

Don’t make me think.

具體地,我根據(jù)這些年來碰到的大量(反面和正面)案例,歸納出以下這些要點。按照要求從低到高的順序如下:

  • 達標(biāo):詞法和語法
    • 正確拼寫
    • 準(zhǔn)確用詞
    • 注意單復(fù)數(shù)
    • 不要搞錯詞性
    • 處理縮寫
    • 用對時態(tài)和語態(tài)
  • 進階:語義和可用性
    • 單一職責(zé)
    • 避免副作用
    • 合理設(shè)計函數(shù)參數(shù)
    • 合理運用函數(shù)重載
    • 使返回值可預(yù)期
    • 固化術(shù)語表
    • 遵循一致的 API 風(fēng)格
  • 卓越:系統(tǒng)性和大局觀
    • 版本控制
    • 確保向下兼容
    • 設(shè)計擴展機制
    • 控制 API 的抽象級別
    • 收斂 API 集
    • 發(fā)散 API 集
    • 制定 API 的支持策略

(本文主要以 JavaScript 作為語言示例。)

達標(biāo):詞法和語法

高級語言和自然語言(英語)其實相差無幾,因此正確地使用(英語的)詞法和語法是程序員最基本的素養(yǎng)。而涉及到 API 這種供用戶調(diào)用的代碼時,則尤其重要。

但事實上,由于亞洲地區(qū)對英語的掌握能力普遍一般……所以現(xiàn)實狀況并不樂觀 —— 如果以正確使用詞法和語法作為達標(biāo)的門檻,很多 API 都沒能達標(biāo)。

正確拼寫

正確地拼寫一個單詞是底線,這一點無需贅述。然而 API 中的各種錯別字現(xiàn)象仍屢見不鮮,即使是在我們阿里這樣的大公司內(nèi)。

曾經(jīng)有某個 JSON 接口(mtop)返回這樣一組店鋪數(shù)據(jù),以在前端模板中渲染:

1
2
3
4
5
6
7
8
9
10
// json
[
{
"shopBottom": {
"isTmall": "false",
"shopLevel": "916",
"shopLeveImg": "http://xxx.jpg"
}
}
]

乍一看平淡無奇,結(jié)果我調(diào)試了小半天都沒能渲染出店鋪的「店鋪等級標(biāo)識圖片」,即 shopLevelImg 字段。問題到底出在了哪里?

眼細的朋友可能已經(jīng)發(fā)現(xiàn),接口給的字段名是 shopLeveImg,少了一個 l,而在其后字母 I 的光輝照耀下,肉眼很難分辨出這個細節(jié)問題。

拼錯單詞的問題真的是太普遍了,再比如:

  • 某個叫做 toast 的庫,package.json 中的 name 寫成了 taost。導(dǎo)致在 npm 中沒能找到這個包。
  • 某個跑馬燈組件,工廠方法中的一個屬性錯將 panel 寫成了 pannel。導(dǎo)致以正確的屬性名初始化時代碼跑不起來。
  • 某個 URL(www.ruanyifeng.com/blog/2017/01/entainment.html)中錯將 entertainment 寫成了 entainment……這倒沒什么大影響,只是 URL 發(fā)布后就改不了了,留下了錯別字不好看。
  • ……

注意到,這些拼寫錯誤經(jīng)常出現(xiàn)在 字符串 的場景中。不同于變量名,IDE 無法檢查字符串中的單詞是否科學(xué)、是否和一些變量名一致,因此,我們在對待一些需要公開出去的 API 時,需要尤其注意這方面的問題;另一方面,更認(rèn)真地注意 IDE 的 typo 提示(單詞拼寫錯誤提示),也會對我們產(chǎn)生很大幫助。

準(zhǔn)確用詞

我們知道,中英文單詞的含義并非一一對應(yīng),有時一個中文意思可以用不同的英文單詞來解釋,這時我們需要選擇使用恰當(dāng)?shù)臏?zhǔn)確的詞來描述。

比如中文的「消息」可以翻譯為 message、notification、news 等。雖然這幾個不同的單詞都可以有「消息」的意思,但它們在用法和語境場景上存在著細微差異:

  • message:一般指雙方通信的消息,是內(nèi)容載體。而且經(jīng)常有來有往、成對出現(xiàn)。比如 postMessage()receiveMessage()。
  • notification:經(jīng)常用于那種比較短小的通知,現(xiàn)在甚至專指 iOS / Android 那樣的通知消息。比如 new NotificationManager()。
  • news:內(nèi)容較長的新聞消息,比 notification 更重量級。比如 getTopNews()
  • feed:自從 RSS 訂閱時代出現(xiàn)的一個單詞,現(xiàn)在 RSS 已經(jīng)日薄西山,但是 feed 這個詞被用在了更多的地方。其含義只可意會不可言傳。比如 fetchWeitaoFeeds()

所以,即使中文意思大體相近,也要準(zhǔn)確地用詞,從而讓讀者更易理解 API 的作用和 上下文場景。

有一個正面案例,是關(guān)于 React 的。(在未使用 ES2015 的)React 中,有兩個方法叫做:

1
2
3
4
5
6
7
8
React.createClass({
getDefaultProps: function() {
// return a dictionary
},
getInitialState: function() {
// return a dictionary either
}
});

它們的作用都是用來定義初始化的組件信息,返回值的類型也都一樣,但是在方法名上卻分別用了 defaultinitial 來修飾,為什么不統(tǒng)一為一個呢?

原因和 React 的機制有關(guān):

  • props 是指 Element 的屬性,要么是不存在某個屬性值后來為它賦值,要么是存在屬性的默認(rèn)值后來將其覆蓋。所以這種行為,default 是合理的修飾詞。
  • state 是整個 Component 狀態(tài)機中的某一個特定狀態(tài),既然描述為了狀態(tài)機,那么狀態(tài)和狀態(tài)之間是互相切換的關(guān)系。所以對于初始狀態(tài),用 initial 來修飾。

就這么個小小的細節(jié),就可一瞥 React 本身的機制,足以體現(xiàn) API 設(shè)計者的智慧。

另外,最近我還碰到了這樣一組事件 API:

1
2
3
4
5
// event name 1
page.emit('pageShowModal');

// event name 2
page.emit('pageCloseModal');

這兩個事件顯然是一對正反義的動作,在上述案例中,表示「顯示窗口」時使用了 show,表示「關(guān)閉窗口」時使用了 close,這都是非常直覺化的直譯。而事實上,成對出現(xiàn)的詞應(yīng)該是:show & hide、open & close。

因此這里必須強調(diào):成對出現(xiàn)的正反義詞不可混用。在程序世界經(jīng)常成對出現(xiàn)的詞還有:

  • in & out
  • on & off
  • previous & next
  • forward & backward
  • success & failure

總之,我們可以試著擴充英語的詞匯量,使用合適的詞,這對我們準(zhǔn)確描述 API 有很大的幫助。

注意單復(fù)數(shù)

所有涉及到諸如數(shù)組(Array)、集合(Collection)、列表(List)這樣的數(shù)據(jù)結(jié)構(gòu),在命名時都要使用復(fù)數(shù)形式:

1
2
3
4
5
6
7
8
9
10
11
12
var shopItems = [
// ...
];

export function getShopItems() {
// return an array
}

// fail
export function getShopItem() {
// unless you really return a non-array
}

現(xiàn)實往往出人意表地糟糕,前不久剛改一個項目,我就碰到了這樣的寫法:

1
2
3
4
5
6
7
class MarketFloor extends Component {
state = {
item: [
{}
]
};
}

這里的 item 實為一個數(shù)組,即使它內(nèi)部只有一個成員。因此應(yīng)該命名為 itemsitemList,無論如何,不應(yīng)該是表示單數(shù)的 item。

同時要注意,在復(fù)數(shù)的風(fēng)格上保持一致,要么所有都是 -s,要么所有都是 -list。

反過來,我們在涉及到諸如字典(Dictionary)、表(Map)的時候,不要使用復(fù)數(shù)!

1
2
3
4
5
6
// fail
var EVENT_MAPS = {
MODAL_WILL_SHOW: 'modalWillShow',
MODAL_WILL_HIDE: 'modalWillHide',
// ...
};

雖然這個數(shù)據(jù)結(jié)構(gòu)看上去由很多 key-value 對組成,是個類似于集合的存在,但是「map」本身已經(jīng)包含了這層意思,不需要再用復(fù)數(shù)去修飾它。

不要搞錯詞性

另外一個容易犯的低級錯誤是搞錯詞性,即命名時拎不清名詞、動詞、形容詞……

1
2
3
4
asyncFunc({
success: function() {},
fail: function() {}
});

success 算是一個在程序界出鏡率很高的詞了,但是有些同學(xué)會搞混,把它當(dāng)做動詞來用。在上述案例中,成對出現(xiàn)的單詞其詞性應(yīng)該保持一致,這里應(yīng)該寫作 succeedfail;當(dāng)然,在這個語境中,最好遵從慣例,使用名詞組合 successfailure。

這一對詞全部的詞性如下:

  • n. 名詞:success, failure
  • v. 動詞:succeed, fail
  • adj. 形容詞:successful, failed(無形容詞,以過去分詞充當(dāng))
  • adv. 副詞:successfully, fail to do sth.(無副詞,以不定式充當(dāng))

注意到,如果有些詞沒有對應(yīng)的詞性,則考慮變通地采用其他形式來達到同樣的意思。

所以,即使我們大部分人都知道:方法命名用動詞、屬性命名用名詞、布爾值類型用形容詞(或等價的表語),但由于對某些單詞的詞性不熟悉,也會導(dǎo)致最終的 API 命名有問題,這樣的話就很尷尬了。

處理縮寫

關(guān)于詞法最后一個要注意的點是縮寫。有時我們經(jīng)常會糾結(jié),首字母縮寫詞(acronym)如 DOMSQL 是用大寫還是小寫,還是僅首字母大寫,在駝峰格式中又該怎么辦……

對于這個問題,簡單不易混淆的做法是,首字母縮寫詞的所有字母均大寫。(如果某個語言環(huán)境有明確的業(yè)界慣例,則遵循慣例。)

1
2
3
4
5
// before
export function getDomNode() {}

// after
export function getDOMNode() {}

在經(jīng)典前端庫 KISSY 的早期版本中,DOM 在 API 中都命名為 dom,駝峰下變?yōu)?Dom;而在后面的版本內(nèi)統(tǒng)一寫定為全大寫的 DOM。

另外一種縮寫的情況是對長單詞簡寫(shortened word),如 btn (button)、chk (checkbox)、tpl (template)。這要視具體的語言規(guī)范 / 開發(fā)框架規(guī)范而定。如果什么都沒定,也沒業(yè)界慣例,那么把單詞寫全了總是不會錯的。

用對時態(tài)和語態(tài)

由于我們在調(diào)用 API 時一般類似于「調(diào)用一條指令」,所以在語法上,一個函數(shù)命名是祈使句式,時態(tài)使用一般現(xiàn)在時。

但在某些情況下,我們需要使用其他時態(tài)(進行時、過去時、將來時)。比如,當(dāng)我們涉及到 生命周期事件節(jié)點。

在一些組件系統(tǒng)中,必然涉及到生命周期,我們來看一下 React 的 API 是怎么設(shè)計的:

1
2
3
4
5
export function componentWillMount() {}
export function componentDidMount() {}
export function componentWillUpdate() {}
export function componentDidUpdate() {}
export function componentWillUnmount() {}

React 劃分了幾個關(guān)鍵的生命周期節(jié)點(mount, update, unmount, …),以將來時和過去時描述這些節(jié)點片段,暴露 API。注意到一個小細節(jié),React 采用了 componentDidMount 這種過去時風(fēng)格,而沒有使用 componentMounted,從而跟 componentWillMount 形成對照組,方便記憶。

同樣地,當(dāng)我們設(shè)計事件 API 時,也要考慮使用合適的時態(tài),特別是希望提供精細的事件切面時。或者,引入 before、after 這樣的介詞來簡化:

1
2
3
4
5
6
7
8
// will render
Component.on('beforeRender', function() {});

// now rendering
Component.on('rendering', function() {});

// has rendered
Component.on('afterRender', function() {});

另一方面是關(guān)于語態(tài),即選用主動語態(tài)和被動語態(tài)的問題。其實最好的原則就是 盡量避免使用被動語態(tài)。因為被動語態(tài)看起來會比較繞,不夠直觀,因此我們要將被動語態(tài)的 API 轉(zhuǎn)換為主動語態(tài)。

寫成代碼即形如:

1
2
3
4
5
// passive voice, make me confused
object.beDoneSomethingBy(subject);

// active voice, much more clear now
subject.doSomething(object);

進階:語義和可用性

說了那么多詞法和語法的注意點,不過才是達標(biāo)級別而已。確保 API 的可用性和語義才使 API 真正「可用」。

無論是友好的參數(shù)設(shè)置,還是讓人甜蜜蜜的語法糖,都體現(xiàn)了程序員的人文關(guān)懷。

單一職責(zé)

單一職責(zé)是軟件工程中一條著名的原則,然而知易行難,一是我們對于具體業(yè)務(wù)邏輯中「職責(zé)」的劃分可能存在難度,二是部分同學(xué)仍沒有養(yǎng)成貫徹此原則的習(xí)慣。

小到函數(shù)級別的 API,大到整個包,保持單一核心的職責(zé)都是很重要的一件事。

1
2
3
4
5
6
// fail
component.fetchDataAndRender(url, template);

// good
var data = component.fetchData(url);
component.render(data, template);

如上,將混雜在一個大坨函數(shù)中的兩件獨立事情拆分出去,保證函數(shù)(function)級別的職責(zé)單一。

更進一步地,(假設(shè))fetchData 本身更適合用另一個類(class)來封裝,則對原來的組件類 Component 再進行拆分,將不屬于它的取數(shù)據(jù)職責(zé)也分離出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DataManager {
fetchData(url) {}
}

class Component {
constructor() {
this.dataManager = new DataManager();
}
render(data, template) {}
}

// more code, less responsibility
var data = component.dataManager.fetchData(url);
component.render(data, template);

在文件(file)層面同樣如此,一個文件只編寫一個類,保證文件的職責(zé)單一(當(dāng)然這對很多語言來說是天然的規(guī)則)。

最后,視具體的業(yè)務(wù)關(guān)聯(lián)度而決定,是否將一簇文件做成一個包(package),或是拆成多個。

避免副作用

嚴(yán)格「無 副作用 的編程」幾乎只出現(xiàn)在純函數(shù)式程序中,現(xiàn)實中的 OOP 編程場景難免觸及副作用。因此在這里所說的「避免副作用」主要指的是:

  1. 函數(shù)本身的運行穩(wěn)定可預(yù)期。
  2. 函數(shù)的運行不對外部環(huán)境造成意料外的污染。

對于無副作用的純函數(shù)而言,輸入同樣的參數(shù),執(zhí)行后總能得到同樣的結(jié)果,這種冪等性使得一個函數(shù)無論在什么上下文中運行、運行多少次,最后的結(jié)果總是可預(yù)期的 —— 這讓用戶非常放心,不用關(guān)心函數(shù)邏輯的細節(jié)、考慮是否應(yīng)該在某個特定的時機調(diào)用、記錄調(diào)用的次數(shù)等等。希望我們以后設(shè)計的 API 不會出現(xiàn)這個案例中的情況:

1
2
3
4
5
// return x.x.x.1 while call it once
this.context.getSPM();

// return x.x.x.2 while call it twice
this.context.getSPM();

在這里,getSPM() 用來獲取每個鏈接唯一的 SPM 碼(SPM 是阿里通用的埋點統(tǒng)計方案)。但是用法卻顯得詭異:每調(diào)用一次,就會返回一個不同的 SPM 串,于是當(dāng)我們需要獲得幾個 SPM 時,就會這樣寫:

1
2
3
var spm1 = this.context.getSPM();
var spm2 = this.context.getSPM();
var spm3 = this.context.getSPM();

雖然在實現(xiàn)上可以理解 —— 此函數(shù)內(nèi)部維護了一個計數(shù)器,每次返回一個自增的 SPM D 位,但是 這樣的實現(xiàn)方式與這個命名看似是冪等的 getter 型函數(shù)完全不匹配,換句話說,這使得這個 API 不可預(yù)期。

如何修改之?一種做法是,不改變此函數(shù)內(nèi)部的實現(xiàn),而是將 API 改為 Generator 式的風(fēng)格,通過形如 SPMGenerator.next() 接口來獲取自增的 SPM 碼。

另一種做法是,如果要保留原名稱,可以將函數(shù)簽名改為 getSPM(spmD),接受一個自定義的 SPM D 位,然后返回整個 SPM 碼。這樣在調(diào)用時也會更明確。

除了函數(shù)內(nèi)部的運行需可預(yù)期外,它對外部一旦造成不可預(yù)期的污染,那么影響將更大,而且更隱蔽。

對外部造成污染一般是兩種途徑:一是在函數(shù)體內(nèi)部直接修改外部作用域的變量,甚至全局變量;二是通過修改實參間接影響到外部環(huán)境,如果實參是引用類型的數(shù)據(jù)結(jié)構(gòu)。

曾經(jīng)也有發(fā)生因為對全局變量操作而導(dǎo)致整個容器垮掉的情況,這里就不再展開。

如何防止此類副作用發(fā)生?本質(zhì)上說,需要控制讀寫權(quán)限。比如:

  1. 模塊沙箱機制,嚴(yán)格限定模塊對外部作用域的修改;
  2. 對關(guān)鍵成員作訪問控制(access control),凍結(jié)寫權(quán)限等等。

合理設(shè)計函數(shù)參數(shù)

對一個函數(shù)來說,「函數(shù)簽名」(Function Signature)比函數(shù)體本身更重要。函數(shù)名、參數(shù)設(shè)置、返回值類型,這三要素構(gòu)成了完整的函數(shù)簽名。而其中,參數(shù)設(shè)置對用戶來說是接觸最頻繁,也最為關(guān)心的部分。

那如何優(yōu)雅地設(shè)計函數(shù)的入口參數(shù)呢?我的理解是這樣幾個要點:

優(yōu)化參數(shù)順序。相關(guān)性越高的參數(shù)越要前置。

這很好理解,相關(guān)性越高的參數(shù)越重要,越要在前面出現(xiàn)。其實這還有兩個隱含的意思,即 可省略的參數(shù)后置,以及 為可省略的參數(shù)設(shè)定缺省值。對某些語言來說(如 C++),調(diào)用的時候如果想省略實參,那么一定要為它定義缺省值,而帶缺省值的參數(shù)必須后置,這是在編譯層面就規(guī)定死的。而對另一部分靈活的語言來說(如 JS),將可省參數(shù)后置同樣是最佳實踐。

1
2
3
4
5
6
7
8
9
10
11
// bad
function renderPage(pageIndex, pageData) {}

renderPage(0, {});
renderPage(1, {});

// good
function renderPage(pageData, pageIndex = 0) {}

renderPage({});
renderPage({}, 1);

第二個要點是控制參數(shù)個數(shù)。用戶記不住過多的入口參數(shù),因此,參數(shù)能省略則省略,或更進一步,合并同類型的參數(shù)。

由于可以方便地創(chuàng)建 Object 這種復(fù)合數(shù)據(jù)結(jié)構(gòu),合并參數(shù)的這種做法在 JS 中尤為普遍。常見的情況是將很多配置項都包成一個配置對象:

1
2
3
4
5
6
7
8
9
10
// traditional
$.ajax(url, params, success);

// or
$.ajax({
url,
params,
success,
failure
});

這樣做的好處是:

  • 用戶雖然仍需記住參數(shù)名,但不用再關(guān)心參數(shù)順序。
  • 不必擔(dān)心參數(shù)列表過長。將參數(shù)合并為字典這種結(jié)構(gòu)后,想增加多少參數(shù)都可以,也不用關(guān)心需要將哪些可省略的參數(shù)后置的問題。

當(dāng)然,凡事有利有弊,由于缺乏順序,就無法突出哪些是最核心的參數(shù)信息;另外,在設(shè)定參數(shù)的默認(rèn)值上,會比參數(shù)列表的形式更繁瑣。因此,需要兼顧地使用最優(yōu)的辦法來設(shè)計函數(shù)參數(shù),為了同一個目的:易用。

合理運用函數(shù)重載

談到 API 的設(shè)計,尤其是函數(shù)的設(shè)計,總離不開一個機制:重載(overload)。

對于強類型語言來說,重載是個很 cool 的功能,能夠大幅減少函數(shù)名的數(shù)量,避免命名空間的污染。然而對于弱類型語言而言,由于不需要在編譯時做 type-binding,函數(shù)在調(diào)用階段想怎么傳實參都行……所以重載在這里變得非常微妙。以下著重談一下,什么時候該選擇重載,什么時候又不該。

1
2
3
4
5
Element getElementById(String: id)

HTMLCollection getElementsByClassName(String: names)

HTMLCollection getElementsByTagName(String: name)

以上三個函數(shù)是再經(jīng)典不過的 DOM API,而在當(dāng)初學(xué)習(xí)它們的時候(從 Java 思維轉(zhuǎn)到 JS 思維)我就在想這兩個問題:

  1. 為什么要設(shè)計成 getSomethingBySomething 這么復(fù)雜結(jié)構(gòu)的名字,而不是使用 getSomething 做重載?
  2. 這三個函數(shù)只有 getElementById 是單數(shù)形式,為何不設(shè)計為返回 HTMLCollection(即使只返回一個成員也可以包一個 Collection 嘛),以做成復(fù)數(shù)形式的函數(shù)名從而保持一致性?

兩個問題中,如果第二個問題能解決,那么這三個函數(shù)的結(jié)構(gòu)將完全一致,從而可以考慮解決第一個問題。

先來看問題二。稍微深入下 DOM 知識后就知道,id 對于整個 DOM 來說必須是唯一的,因此在理論上 getElementsById(注意有復(fù)數(shù))將永遠返回僅有 0 或 1 個成員的 Collection,這樣一來用戶的調(diào)用方式將始終是 var element = getElementsById(id)[0],而這是非常荒謬的。所以 DOM API 設(shè)計得沒問題。

既然問題二無解,那么自然這三個函數(shù)沒法做成一個重載。退一步說,即使問題二能解決,還存在另外一個麻煩:它們的入口參數(shù)都是一樣的,都是 String!對于強類型語言來說,參數(shù)類型和順序、返回值統(tǒng)統(tǒng)一樣的情況下,壓根無法重載。因為編譯器無法通過任何一個有效的特征,來執(zhí)行不同的邏輯!

所以,如果入口參數(shù)無法進行有效區(qū)分,不要選擇重載。

當(dāng)然,有一種奇怪的做法可以繞過去:

1
2
3
4
5
6
7
8
9
10
11
12
// fail
function getElementsBy(byWhat, name) {
switch(byWhat) {
case 'className':
// ...
case 'tagName':
// ...
}
}

getElementsBy('tagName', name);
getElementsBy('className', name);

一種在風(fēng)格上類似重載的,但實際是在運行時走分支邏輯的做法……可以看到,API 的信息總量并沒降低。不過話不能說死,這種風(fēng)格在某些特定場景也有用武之地,只是多數(shù)情況下并不推薦。

與上述風(fēng)格類似的,是這樣一種做法:

1
2
3
4
5
// get elements by tag-name by default
HTMLCollection getElements(String: name)

// if you add a flag, it goes by class-name
HTMLCollection getElements(String: name, Boolean: byClassName)

「將 flag 標(biāo)記位作為了重載手段」—— 在早期微軟的一些 API 中經(jīng)常能見到這樣的寫法,可以說一旦離開了文檔就無法編碼,根本不明白某個 Boolean 標(biāo)記位是用來干嘛的,這大大降低了用戶的開發(fā)體驗,以及代碼可讀性。

這樣看起來,可重載的場景真是太少了!也不盡然,在我看來有一種場景很適合用重載:批量處理。

1
2
3
Module handleModules(Module: module)

Collection<Module> handleModules(Collection<Module>: modules)

當(dāng)用戶經(jīng)常面臨處理一個或多個不確定數(shù)量的對象時,他可能需要思考和判斷,什么時候用單數(shù) handleModule、什么時候用復(fù)數(shù) handleModules。將這種類型的操作重載為一個(大抵見于 setter 型操作),同時支持單個和批量的處理,可以降低用戶的認(rèn)知負擔(dān)。

所以,在合適的時機重載,否則寧愿選擇「函數(shù)名結(jié)構(gòu)相同的多個函數(shù)」。原則是一樣的,保證邏輯正確的前提下,盡可能降低用戶負擔(dān)。

對了,關(guān)于 getElements 那三個 API,它們最終的進化版本回到了同一個函數(shù):querySelector(selectors)。

使返回值可預(yù)期

函數(shù)的易用性體現(xiàn)在兩方面:入口和出口。上面已經(jīng)講述了足夠多關(guān)于入口的設(shè)計事項,這一節(jié)講出口:函數(shù)返回值。

對于 getter 型的函數(shù)來說,調(diào)用的直接目的就是為了獲得返回值。因此我們要讓返回值的類型和函數(shù)名的期望保持一致。

1
2
3
4
5
6
7
8
// expect 'a.b.c.d'
function getSPMInString() {

// fail
return {
a, b, c, d
};
}

從這一點上來講,要慎用 ES2015 中的新特性「解構(gòu)賦值」。

而對于 setter 型的函數(shù),調(diào)用的期望是它能執(zhí)行一系列的指令,然后去達到一些副作用,比如存文件、改寫變量值等等。因此絕大多數(shù)情況我們都選擇了返回 undefined / void —— 這并不總是最好的選擇。

回想一下,我們在調(diào)用操作系統(tǒng)的命令時,系統(tǒng)總會返回「exit code」,這讓我們能夠獲知系統(tǒng)命令的執(zhí)行結(jié)果如何,而不必通過其他手段去驗證「這個操作到底生效了沒」。因此,創(chuàng)建這樣一種返回值風(fēng)格,或可一定程度增加健壯性。

另外一個選項,是讓 setter 型 API 始終返回 this。這是 jQuery 為我們帶來的經(jīng)典啟示 —— 通過返回 this,來產(chǎn)生一種「鏈?zhǔn)秸{(diào)用(chaining)」的風(fēng)格,簡化代碼并且增加可讀性:

1
2
3
4
$('div')
.attr('foo', 'bar')
.data('hello', 'world')
.on('click', function() {});

最后還有一個異類,就是異步執(zhí)行的函數(shù)。由于異步的特性,對于這種需要一定延時才能得到的返回值,只能使用 callback 來繼續(xù)操作。使用 Promise 來包裝它們尤為必要。對異步操作都返回一個 Promise,使整體的 API 風(fēng)格更可預(yù)期。

固化術(shù)語表

在前面的詞法部分中曾經(jīng)提到「準(zhǔn)確用詞」,但即使我們已經(jīng)盡量去用恰當(dāng)?shù)脑~,在有些情況下仍然不免碰到一些難以抉擇的尷尬場景。

比如,我們經(jīng)常會看到 pic 和 image、path 和 url 混用的情況,這兩組詞的意思非常接近(當(dāng)然嚴(yán)格來說 path 和 url 的意義是明確不同的,在此暫且忽略),稍不留神就會產(chǎn)生 4 種組合……

  • picUrl
  • picPath
  • imageUrl
  • imagePath
  • 更糟糕的情況是 imgUrl、picUri、picURL……

所以,在一開始就要 產(chǎn)出術(shù)語表,包括對縮寫詞的大小寫如何處理、是否有自定義的縮寫詞等等。一個術(shù)語表可以形如:

標(biāo)準(zhǔn)術(shù)語含義禁用的非標(biāo)準(zhǔn)詞
pic圖片image, picture
path路徑URL, url, uri
on綁定事件bind, addEventListener
off解綁事件unbind, removeEventListener
emit觸發(fā)事件fire, trigger
module模塊mod

不僅在公開的 API 中要遵守術(shù)語表規(guī)范,在局部變量甚至字符串中都最好按照術(shù)語表來。

1
2
3
4
page.emit('pageRenderRow', {
index: this.props.index,
modList: moduleList
});

比如這個我最近碰到的案例,同時寫作了 modListmoduleList,這就有點怪怪的。

另外,對于一些創(chuàng)造出來的、業(yè)務(wù)特色的詞匯,如果不能用英語簡明地翻譯,就直接用拼音:

  • 淘寶 Taobao
  • 微淘 Weitao
  • 極有家 Jiyoujia
  • ……

在這里,千萬不要把「微淘」翻譯為 MicroTaobao……當(dāng)然,專有詞已經(jīng)有英文名的除外,如 Tmall

遵循一致的 API 風(fēng)格

這一節(jié)算得上是一個復(fù)習(xí)章節(jié)。詞法、語法、語義中的很多節(jié)都指向同一個要點:一致性。

一致性可以最大程度降低信息熵。

好吧,這句話不是什么名人名言,就是我現(xiàn)編的??偠灾?,一致性能大大降低用戶的學(xué)習(xí)成本,并對 API 產(chǎn)生準(zhǔn)確的預(yù)期。

  1. 在詞法上,提煉術(shù)語表,全局保持一致的用詞,避免出現(xiàn)不同的但是含義相近的詞。
  2. 在語法上,遵循統(tǒng)一的語法結(jié)構(gòu)(主謂賓順序、主被動語態(tài)),避免天馬行空的造句。
  3. 在語義上,合理運用函數(shù)的重載,提供可預(yù)期的甚至一致類型的函數(shù)入口和出口。

甚至還可以一致得更細節(jié)些,只是舉些例子:

  1. 打 log 要么都用中文,要么都用英文。
  2. 異步接口要么都用回調(diào),要么都改成 Promise。
  3. 事件機制只能選擇其一:object.onDoSomething = funcobject.on('doSomething', func)。
  4. 所有的 setter 操作必須返回 this
  5. ……

一份代碼寫得再怎么爛,把某個單詞都拼成一樣的錯誤,也好過這個單詞只出現(xiàn)一次錯誤。

是的,一致性,再怎么強調(diào)都不為過。

卓越:系統(tǒng)性和大局觀

不管是大到發(fā)布至業(yè)界,或小到在公司內(nèi)跨部門使用,一組 API 一旦公開,整體上就是一個產(chǎn)品,而調(diào)用方就是用戶。所謂牽一發(fā)而動全身,一個小細節(jié)可能影響整個產(chǎn)品的面貌,一個小改動也可能引發(fā)整個產(chǎn)品崩壞。因此,我們一定要站在全局的層面,甚至考慮整個技術(shù)環(huán)境,系統(tǒng)性地把握整個體系內(nèi) API 的設(shè)計,體現(xiàn)大局觀。

版本控制

80% 的項目開發(fā)在版本控制方面做得都很糟糕:隨心所欲的版本命名、空洞詭異的提交信息、毫無規(guī)劃的功能更新……人們顯然需要一段時間來培養(yǎng)規(guī)范化開發(fā)的風(fēng)度,但是至少得先保證一件事情:

在大版本號不變的情況下,API 保證向前兼容。

這里說的「大版本號」即「語義化版本命名」<major>.<minor>.<patch> 中的第一位 <major> 位。

這一位的改動表明 API 整體有大的改動,很可能不兼容,因此用戶對大版本的依賴改動會慎之又慎;反之,如果 API 有不兼容的改動,意味著必須修改大版本號,否則用戶很容易出現(xiàn)在例行更新依賴后整個系統(tǒng)跑不起來的情況,更糟糕的情況則是引發(fā)線上故障。

如果這種情況得不到改善,用戶們就會選擇 永遠不升級依賴,導(dǎo)致更多的潛在問題。久而久之,最終他們便會棄用這些產(chǎn)品(庫、中間件、whatever)。

所以,希望 API 的提供者們以后不會再將大版本鎖定為 0。更多關(guān)于「語義化版本」的內(nèi)容,請參考我的另一篇文章《論版本號的正確打開方式》。

確保向下兼容

如果不希望對客戶造成更新升級方面的困擾,我們首先要做好的就是確保 API 向下兼容。

API 發(fā)生改動,要么是需要提供新的功能,要么是為之前的糟糕設(shè)計買單……具體來說,改動無外乎:增加、刪除、修改 三方面。

首先是刪除。不要輕易刪除公開發(fā)布的 API,無論之前寫得多么糟糕。如果一定要刪除,那么確保正確使用了「Deprecated」:

對于某個不想保留的可憐 API,先不要直接刪除,將其標(biāo)記為 @deprecated 后置入下一個小版本升級(比如從 1.0.21.1.0)。

1
2
3
4
5
6
7
8
9
/**
* @deprecated
*/
export function youWantToRemove(foo, bar) {}

/**
* This is the replacement.
*/
export function youWantToKeep(foo) {}

并且,在 changelog 中明確指出這些 API 即將移除(不推薦使用,但是目前仍然能用)。

之后,在下一個 大版本 中(比如 1.1.02.0.0)刪除標(biāo)記為 @deprecated 的部分,同時在 changelog 中指明它們已刪除。

其次是 API 的修改。如果我們僅僅是修復(fù) bug、重構(gòu)實現(xiàn)、或者添加一些小特性,那自然沒什么可說的;但是如果想徹底修改一個 API……比如重做入口參數(shù)、改寫業(yè)務(wù)邏輯等等,建議的做法是:

  1. 確保原來的 API 符合「單一職責(zé)」原則,如果不是則修改之。
  2. 增加一個全新的 API 去實現(xiàn)新的需求!由于我們的 API 都遵循「單一職責(zé)」,因此一旦需要徹底修改 API,意味著新需求和原來的職責(zé)已經(jīng)完全無法匹配,不如干脆新增一個 API。
  3. 視具體情況選擇保留或移除舊 API,進入前面所述「刪除 API」的流程。

最后是新增 API。事實上,即使是只加代碼不刪代碼,整體也不一定是向下兼容的。有一個經(jīng)典的正面案例是:

1
2
3
4
5
// modern browsers
document.hidden == false;

// out-of-date browsers
document.hidden == undefined;

瀏覽器新增的一個 API,用以標(biāo)記「當(dāng)前文檔是否可見」。直觀的設(shè)計應(yīng)該是新增 document.visible 這樣的屬性名……問題是,在邏輯上,文檔默認(rèn)是可見的,即 document.visible 默認(rèn)為 true,而不支持此新屬性的舊瀏覽器返回 document.visible == undefined,是個 falsy 值。因此,如果用戶在代碼中簡單地以:

1
2
3
if (document.visible) {
// do some stuff
}

做特征檢測的話,在舊瀏覽器中就會進入錯誤的條件分支……而反之,以 document.hidden API 來判斷,則是向下兼容的。

設(shè)計擴展機制

毫無疑問,在保證向下兼容的同時,API 需要有一個對應(yīng)的擴展機制以可持續(xù)發(fā)展 —— 一方面便于開發(fā)者自身增加功能,另一方面用戶也能參與進來共建生態(tài)。

技術(shù)上來說,接口的擴展方式有很多,比如:繼承(extend)、組合(mixin)、裝飾(decorate)……選擇沒有對錯,因為不同的擴展方式適用于不同的場景:在邏輯上確實存在派生關(guān)系,并且需要沿用基類行為同時自定義行為的,采用重量級的繼承;僅僅是擴充一些行為功能,但是邏輯上壓根不存在父子關(guān)系的,使用組合;而裝飾手法更多應(yīng)用于給定一個接口,將其包裝成多種適用于不同場景新接口的情況……

另一方面,對于不同的編程語言來說,由于不同的語言特性……靜態(tài)、動態(tài)等,各自更適合用某幾種擴展方式。所以,到底采用什么擴展辦法,還是得視情況而定。

在 JS 界,有一些經(jīng)典的技術(shù)產(chǎn)品,它們的擴展甚至已經(jīng)形成生態(tài),如:

  • jQuery。耳熟能詳?shù)?$.fn.customMethod = function() {};。這種簡單的 mixin 做法已經(jīng)為 jQuery 提供了成千上萬的插件,而 jQuery 自己的大部分 API 本身也是基于這個寫法構(gòu)建起來的。
  • React。React 自身已經(jīng)處理了所有有關(guān)組件實例化、生命周期、渲染和更新等繁瑣的事項,只要開發(fā)者基于 React.Component 來繼承出一個組件類。對于一個 component system 來說,這是一個經(jīng)典的做法。
  • Gulp。相比于近兩年的大熱 Webpack,個人認(rèn)為 Gulp 更能體現(xiàn)一個 building system 的邏輯 —— 定義各種各樣的「任務(wù)」,然后用「管道」將它們串起來。一個 Gulp 插件也是那么的純粹,接受文件流,返回文件流,如是而已。
  • Koa。對于主流的 HTTP Server 來說,中間件的設(shè)計大同小異:接受上一個 request,返回一個新的 response。而對天生 Promise 化的 Koa 來說,它的中間件風(fēng)格更接近于 Gulp 了,區(qū)別僅在于一個是 file stream,一個是 HTTP stream。

不只是龐大的框架需要考慮擴展性,設(shè)計可擴展的 API 應(yīng)該變成一種基本的思維方式。比如這個活生生的業(yè)務(wù)例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// json
[
{
"type": "item",
"otherAttrs": "foo"
},
{
"type": "shop",
"otherAttrs": "bar"
}
]

// render logic
switch(feed.type) {
case 'item':
console.log('render in item-style.');
break;
case 'shop':
console.log('render in shop-style.');
break;
case 'other':
default:
console.log('render in other styles, maybe banner or sth.');
break;
}

根據(jù)不同的類型渲染一組 feeds 信息:商品模塊、店鋪模塊,或是其他。某天新增了需求說要支持渲染天貓的店鋪模塊(多顯示個天貓標(biāo)等等),于是 JSON 接口直接新增一個 type = 'tmallShop' —— 這種接口改法很簡單直觀,但是并不好。在不改前端代碼的情況下,tmallShop 類型默認(rèn)進入 default 分支,導(dǎo)致奇奇怪怪的渲染結(jié)果。

考慮到 tmallShopshop 之間是一個繼承的關(guān)系,tmallShop 完全可以當(dāng)一個普通的 shop 來用,執(zhí)行后者的所有邏輯。用 Java 的表達方式來說就是:

1
2
3
// a tmallShop is a shop
Shop tmallShop = new TmallShop();
tmallShop.doSomeShopStuff();

將這個邏輯關(guān)系反映到 JSON 接口中,合理的做法是新增一個 subType 字段,用來標(biāo)記 tmallShop,而它的 type 仍然保持為 shop。這樣一來,即使原來的前端代碼完全不修改,仍然可以正常運行,除了無法渲染出一些天貓店鋪的特征。

這里還有一個非常類似的正面案例,是 ABS 搭建系統(tǒng)(淘寶 FED 出品的站點搭建系統(tǒng))設(shè)計的模塊 JSON Schema:

1
2
3
4
5
6
7
8
9
10
// json
[
{
"type": "string",
"format": "enum"
}, {
"type": "string",
"format": "URL"
}
]

同樣采用了 type 為主類型,而擴展字段在這里變成了 format,用來容納一些擴展特性。在實際開發(fā)中,的確也很方便新增各種新的數(shù)據(jù)結(jié)構(gòu)邏輯。

控制 API 的抽象級別

API 能擴展的前提是什么?是接口足夠抽象。這樣才能夠加上各種具體的定語、裝飾更多功能。用日常語言舉個例子:

1
2
3
4
5
6
7
8
9
// abstract
I want to go to a place.
// when
{Today, Tomorrow, Jan. 1st} I want to go to a place.
// where
I want to go to {mall, cafe, bed}.

// concrete, no extends any more
Today I want to go to a cafe for my business.

所以,在設(shè)計 API 時要高抽象,不要陷入具體的實現(xiàn),不要陷入具體的需求,要高屋建瓴。

看個實際的案例:一個類 React Native 的頁面框架想暴露出一個事件「滾動到第二屏」,以便頁面開發(fā)者能監(jiān)聽這個事件,從而更好地控制頁面資源的加載策略(比如首屏默認(rèn)加載渲染、到第二屏之后再去加載剩下的資源)。

但是因為一些實現(xiàn)上的原因,頁面框架還不能通過頁面位移(offset)來精確地通知「滾動到了第二屏」,而只能判斷「第二屏的第一個模塊出現(xiàn)了」。于是這個事件沒有被設(shè)計為 secondScreenReached,而變成了 secondScreenFirstModuleAppear……雖然 secondScreenFirstModuleAppear 不能精確定義 secondScreenReached,但是直接暴露這個具體的 API 實在太糟糕了,問題在于:

  • 用戶在依賴一個非常非常具體的 API,給用戶造成了額外的信息負擔(dān)?!傅诙恋牡谝粋€模塊出現(xiàn)了!」這很怪異,用戶根本不關(guān)心模塊的事情,用戶關(guān)心的只是他是否到達了第二屏。
  • 一旦頁面框架能夠真正通過頁面位移來實現(xiàn)「滾動到第二屏」,如果我們暴露的是高抽象的 secondScreenReached,那么只需要更改一下這個接口的具體實現(xiàn)即可;反之,我們暴露的是很具體的 secondScreenFirstModuleAppear,就只能挨個通知用戶:「你現(xiàn)在可以不用依賴這個事件了,改成我們新出的 secondScreenReached 吧!」

是的,抽象級別一般來說越高越好,將 API 設(shè)計成業(yè)務(wù)無關(guān)的,更通用,而且方便擴展。但是物極必反,對于像我這樣的抽象控來說,最好能學(xué)會控制接口的抽象級別,將其保持在一個恰到好處的層次上,不要做無休止的抽象。

還是剛才的例子 secondScreenReached,我們還可以將其抽象成 targetScreenReached,可以支持到達首屏、到達第二屏、第三屏……的事件,這樣是不是更靈活、更優(yōu)雅呢?并沒有 ——

  • 抽象時一定要考慮到具體的業(yè)務(wù)需求場景,有些實現(xiàn)路徑如果永遠不可能走到,就沒必要抽出來。比如這個例子中,沒有人會去關(guān)心第三屏、第四屏的事件。
  • 太高的抽象容易造成太多的層次,帶來額外的耦合、通信等不同層次之間的溝通成本,這將會成為新的麻煩。對用戶而言,也是額外的信息負擔(dān)。

對于特定的業(yè)務(wù)來說,接口越抽象越通用,而越具體則越能解決特定問題。所以,思考清楚,API 面向的場景范圍,避免懶惰設(shè)計,避免過度設(shè)計。

收斂 API 集

對于一整個體系的 API 來說,用戶面對的是這個整體集合,而不是其中某幾個單一的 API。我們要保證集合內(nèi)的 API 都在一致的抽象維度上,并且適當(dāng)?shù)睾喜?API,減小整個集合的信息量,酌情做減法。

產(chǎn)品開始做減法,便是對用戶的溫柔。

收斂近似意義的參數(shù)和局部變量。下面這樣的一組 API 好像沒什么不對,但是對強迫癥來說一定產(chǎn)生了不祥的直覺:

1
2
3
4
5
export function selectTab(index) {}

export function highlightTab(tabIndex) {}

export function gotoPage(index) {}

又是 index 又是 tabIndex 的,或許還會有 pageIndex?誠然,函數(shù)形參和局部變量的命名對最終用戶來說沒有直接影響,但是這些不一致的寫法仍然能反映到 API 文檔中,并且,對開發(fā)者自身也會產(chǎn)生混淆。所以,選一個固定的命名風(fēng)格,然后從一而終!如果忘了的話,回頭看一下前文「固化術(shù)語表」這一節(jié)吧!

收斂近似職責(zé)的函數(shù)。對用戶暴露出太多的接口不是好事,但是一旦要合并不同的函數(shù),是否就會破壞「單一職責(zé)」原則呢?

不,因為「單一職責(zé)」本身也要看具體的抽象層次。以下這個例子和前文「合理運用函數(shù)重載」中的例子有相似之處,但具體又有所不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// a complex rendering process
function renderPage() {

// too many APIs here
renderHeader();
renderBody();
renderSidebar();
renderFooter();
}

// now merged
function renderPage() {
renderSections([
'header', 'body', 'sidebar', 'footer'
]);
}

// call renderSection
function renderSections(sections) {}

// and the real labor
function renderSection(section) {}

類似于這樣,避免暴露過多近似的 API,合理利用抽象將其合并,減小對用戶的壓力。

對于一個有清晰繼承樹的場景來說,收斂 API 顯得更加自然且意義重大 —— 利用多態(tài)性(Polymorphism)構(gòu)建 Consistent APIs。(以下例子來源于 Clean Code JS。)

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad: type-checking here
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}

// cool
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}

有一個將 API 收斂到極致的家伙恐怕大家都不會陌生:jQuery 的 $()。這個風(fēng)格不正是 jQuery 當(dāng)年的殺手級特性之一嗎?

如果 $() 能讓我搞定這件事,就不要再給我 foo()bar()

收斂近似功能的包。再往上一級,我們甚至可以合并相近的 package。

淘寶 FED 的 Rax 體系(類 RN 框架)中,有基礎(chǔ)的組件標(biāo)簽,如 <Image> (in @ali/rax-components)、<Link> (in @ali/rax-components),也有一些增強功能的 package,如 <Picture> (in @ali/rax-picture)、<Link> (in @ali/rax-spmlink)。

在這里,后者包之于前者相當(dāng)于裝飾了更多功能,是前者的增強版。而在實際應(yīng)用中,也是推薦使用諸如 <Picture> 而禁止使用 <Image>。那么在這種大環(huán)境下,<Image> 等基礎(chǔ) API 的暴露就反而變得很擾民。可以考慮將增強包的功能完全合并入基礎(chǔ)組件,即將 <Picture> 并入 <Image>,用戶只需面對單一的、標(biāo)準(zhǔn)的組件 API。

發(fā)散 API 集

這聽上去很荒謬,為什么一個 API 集合又要收斂又要發(fā)散?僅僅是為了大綱上的對稱性嗎?

當(dāng)然不是。存在這個小節(jié)是因為我有一個不得不提的案例,不適合放在其他段落,只能放在這里……不,言歸正傳,我們有時的確需要發(fā)散 API 集,提供幾個看似接近的 API,以引導(dǎo)用戶。因為 —— 雖然這聽起來很荒謬 —— 某些情況下,API 其實不夠用,但是用戶 沒有意識到 API 不夠用,而是選擇了混用、濫用。看下面這個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// the func is used here
requestAnimationFrame(() => {

// what? trigger an event?
emitter.emit('moduleDidRenderRow');
});

// ...and there
requestAnimationFrame(() => {

// another one here, I guess rendering?
this.setState({
// ...
});
});

在重構(gòu)一組代碼時,我看到代碼里充斥著 requestAnimationFrame(),這是一個比較新的全局 API,它會以接近 60 FPS 的速率延時執(zhí)行一個傳入的函數(shù),類似于一個針對特定場景優(yōu)化過的 setTimeout(),但它的初衷是用來繪制動畫幀的,而不應(yīng)該用在奇奇怪怪的場景中。

在深入地了解了代碼邏輯之后,我認(rèn)識到這里如此調(diào)用是為了「延時一丟丟執(zhí)行一些操作」,避免阻塞主渲染線程。然而這種情況下,還不如直接調(diào)用 setTimeout() 來做延時操作。雖然沒有太明確的語義,但是至少好過把自己偽裝成一次動畫的繪制。更可怕的是,據(jù)我所知 requestAnimationFrame() 的濫用不僅出現(xiàn)在這次重構(gòu)的代碼中,我至少在三個不同的庫見過它的身影 —— 無一例外地,這些庫和動畫并沒有什么關(guān)系。

(一個可能的推斷是,調(diào)用 requestAnimationFrame(callback) 時不用指定 timeout 毫秒數(shù),而 setTimeout(callback, timeout) 是需要的。似乎對很多用戶來說,前者的調(diào)用方式更 cool?)

所以,在市面上有一些 API 好像是「偏方」一般的存在:雖然不知道為什么要這么用,但是……用它就對了!

事實上,對于上面這個場景,最恰當(dāng)?shù)慕夥ㄊ鞘褂靡粋€更加新的 API,叫做 requestIdleCallback(callback)。這個 API 從名字上看起來就很有語義:在線程空閑的時候再執(zhí)行操作。這完全契合上述場景的需求,而且還自帶底層的優(yōu)化。

當(dāng)然,由于 API 比較新,還不是所有的平臺都能支持。即便如此,我們也可以先面向接口編程,自己做一個 polyfill:

1
2
3
4
// simple polyfill
export function requestIdleCallback(callback) => {
callback && setTimeout(callback, 1e3 / 60);
};

另一個經(jīng)典的濫用例子是 ES2015 中的「Generator / yield」。

原本使用場景非常有限的生成器 Generator 機制被大神匠心獨運地加以改造,包裝成用來異步代碼同步化的解決方案。這種做法自然很有創(chuàng)意,但是從語義用法上來說實在不足稱道,讓代碼變得非常難讀,并且?guī)砭S護隱患。與其如此,還不如僅僅使用 Promise。

令人欣慰的是,隨后新版的 ES 即提出了新的異步代碼關(guān)鍵字「async / await」,真正在語法層面解決了異步代碼同步化的問題,并且,新版的 Node.js 也已經(jīng)支持這種語法。

因此,我們作為 API 的開發(fā)者,一定要提供足夠場景適用的 API,來引導(dǎo)我們的用戶,不要讓他們做出一些出人意料的「妙用」之舉。

制定 API 的支持策略

我們說,一組公開的 API 是產(chǎn)品。而產(chǎn)品,一定有特定的用戶群,或是全球的開發(fā)者,或僅僅是跨部門的同事;產(chǎn)品同時有保質(zhì)期,或者說,生命周期。

面向目標(biāo)用戶群體,我們要制定 API 的支持策略:

  • 每一個大版本的支持周期是多久。
  • 是否有長期穩(wěn)定的 API 支持版本。(Long-term Support)
  • 如何從舊版本升級。

老舊版本很可能還在運行,但維護者已經(jīng)沒時間精力再去管這些歷史遺物,這時明確地指出某些版本不再維護,對開發(fā)者和用戶都好。當(dāng)然,同時別忘了給出升級文檔,指導(dǎo)老用戶如何遷移到新版本。還有一個更好的做法是,在我們開啟一個新版本之際,就確定好上一個版本的壽命終點,提前知會到用戶。

還有一個技術(shù)上的注意事項,那就是:大版本間最好有明確的隔離。對于一個復(fù)雜的技術(shù)產(chǎn)品來說,API 只是最終直接面向用戶的接口,背后還有特定的環(huán)境、工具組、依賴包等各種支撐,互相之間并不能混用。

比如,曾經(jīng)的經(jīng)典前端庫 KISSY。在業(yè)界技術(shù)方案日新月異的大潮下,KISSY 6 版本已經(jīng)強依賴了 TNPM(阿里內(nèi)網(wǎng)的 NPM)、DEF 套件組(淘寶 FED 的前端工具套件),雖然和之前的 1.4 版本相比 API 的變化并不大,但是仍然不能在老環(huán)境下直接使用 6 版本的代碼庫……這一定程度上降低了自由組合的靈活度,但事實上隨著業(yè)務(wù)問題場景的復(fù)雜度提升,解決方案本身會需要更定制化,因此,將環(huán)境、工具等上下游關(guān)聯(lián)物隨代碼一起打包,做成一整個技術(shù)方案,這正是業(yè)界的現(xiàn)狀。

所以,隔離大版本,制定好 API 支持策略,讓我們的產(chǎn)品更專業(yè),讓用戶免去后顧之憂。

總結(jié)

以上,便是我從業(yè)以來感悟到的一些「道」,三個進階層次、幾十個細分要點,不知有沒有給讀者您帶來一丁點啟發(fā)。

但實際上,大道至簡。我一直認(rèn)為,程序開發(fā)和平時的說話寫字其實沒有太大區(qū)別,無非三者 ——

  1. 邏輯和抽象。
  2. 領(lǐng)域知識。
  3. 語感。

寫代碼,就像寫作,而設(shè)計 API 好比列提綱。勤寫、勤思,了解前人的模式、套路,學(xué)習(xí)一些流行庫的設(shè)計方法,掌握英語、提高語感……相信大家都能設(shè)計出卓越的 API。

最后,附上 API 設(shè)計的經(jīng)典原則:

Think about future, design with flexibility, but only implement for production.

引用

  1. Framework Design Guidelines
  2. Page Visibility 的 API 設(shè)計
  3. 我心目中的優(yōu)秀 API
  4. Clean Code JavaScript

題圖:只是一張符合上下文的圖片,并沒有更深的含義。

花絮:由于文章很長,在編寫過程中我也不由得發(fā)生了「同一個意思卻使用多種表達方式」的情況。某些時候這是必要的 —— 可以豐富文字的多樣性;而有些時候,則顯得全文缺乏一致性。在發(fā)表本文之前,我搜索了這些詞語:「調(diào)用者」、「調(diào)用方」、「引用者」、「使用者」,然后將它們統(tǒng)一修改為我們熟悉的名字:「用戶」。

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Jquery鼠標(biāo)右鍵點擊彈出菜單
云風(fēng)的 BLOG: 在 lua 中實現(xiàn)函數(shù)的重載(轉(zhuǎn))
ActionScript3的函數(shù)重載
構(gòu)建自定義組件
五分鐘實例,javascript的函數(shù)參數(shù)和函數(shù)重載【v3008】
什么是 Angular Composable 概念
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服