實現鍵值對儲存(四):API設計

熊鐸發表於2014-12-12

我終於為這個鍵值對儲存專案確定了一個名字,從現在開始我將叫它FelixDB KingDB。(譯註:改成這麼土的名字也是醉了)

在本文中,我將對帶著大家看一看四個鍵值對儲存和資料庫系統的API:LevelDB, Kyoto Cabinet, BerkekeyDB 和 SQLite3。對於其API中的每個主要功能,我將會比較他們的命名習慣和方法原型,以平衡其優缺點併為正在開發的鍵值對儲存KingDB設計API。本文將包括:

  1. API設計的一般準則
  2. 定義KingDB公共API的功能
  3. 比較現有資料庫的API
    3.1 開啟和關閉資料庫
    3.2 讀寫操作
    3.3 遍歷
    3.4 引數處理
    3.5 錯誤管理
  4. 結論
  5. 參考文獻

1.API設計的一般準則

設計一個好的API很難,相當難。但我在這說的不是什麼新東西,而只是在重複之前很多人告訴我的東西。到目前為止我發現的最好的資料是Joshua Bloch的演講“How to Design a Good API & Why it Matters(如何設計一個好的API及為什麼這很重要)”[1],及其摘要版本[2]。如果你還沒有看過這個演講,我強烈建議你找時間去看一下。在這個演講中,Bloch清晰的陳述了聽眾需要記住的兩個很重要的東西。我複製了摘要版本的要點並新增了一些評論:

  1. 不確定的時候,先放一邊。當不確定某功能、類、方法或引數是否要新增在API中的時候,不要新增。
  2. 不要讓使用者做庫可以做的事情。如果你的API讓客戶執行一系列函式呼叫的時候,需要將每個函式的輸出塞到下一個函式的輸入裡,那你應該在API中新增一個函式來執行這一系列的函式呼叫。

另一個關於API設計的好資源是Joshua Bloch寫的《Effective Java》4和Scott Meyers寫的《Effective C++》3第四章“Designs and Declarations”。

這些資源對於當前階段的這個鍵值對儲存專案來說十分重要,儘管我覺得這些資源沒有包含一個很重要的因素:使用者期望。將API從草圖上設計出來是很難的,但這個鍵值對儲存的例子來說,是有例可循的。使用者一直和他們的鍵值對儲存或資料庫系統的API打交道。因此,當面對一個新的鍵值對儲存的時候,使用者希望有一個類似的環境,而不關心這潛規則只會提高使用者對新API的學習曲線,並讓使用者不高興。

鑑於這個原因,即便我牢記上文列出的這些資料中的所有好建議,但我仍認為我必須儘可能多的複製已有庫的API,因為這可以在使用者使用我正建立的API時更簡單。

2.定義KingDB公共API的功能

考慮到這只是萬里長征的第一步,我打算實現一個最小且可靠的鍵值對儲存,我當然不會包含所有的,像Kyoto Cabinet 和LevelDB那樣的成熟專案提供的高階功能。我打算先讓基本功能實現,然後我將逐漸增加其他功能。對於我來說,基本功能嚴格限制在:

  • 開啟和關閉資料庫
  • 讀寫資料庫
  • 遍歷資料庫中所有的鍵值對集合
  • 提供引數調整的方法
  • 提供一個合適的錯誤通知介面

我意識到這些功能對於一些用例來說過於侷限了,但暫時應該對付的過來。我不打算新增任何事務機制、分類查詢、或原子操作。同樣,現在我不打算提供快照功能。

3.比較現有資料庫的API

為了比較現有資料庫的C++ API,我將會比較每個功能的示例程式碼。 這些示例程式碼是修改自或直接取自於官方程式碼“Fundamental Specifications of Kyoto Cabinet” [5], “LevelDB’s Detailed Documentation” [6], “Getting Started with Berkeley DB” [7], 和 “SQLite in 5 minutes or less” [8]。 我同樣會使用不同的顏色來標示來自不同的API。

3.1 開啟和關閉資料庫

下述示例程式碼顯示出研究的系統是如何開啟資料庫的。為了更清晰的顯示程式碼原理,選項設定和錯誤管理沒有在此顯示,並且會在下述各節中解釋更多的細節。

 

在開啟資料庫部分出現了兩種清晰的模式。一方面,LevelDB 和SQLite3的API請求建立一個資料庫物件的指標(控制程式碼)。然後呼叫開啟函式的時候將這個指標的引用作為引數,以定位物件的記憶體空間,然後設定這個資料庫物件。另一方面,Kyoto Cabinet 和Berkeley DB的API以例項化一個資料庫物件為開始,然後調物件的用open()方法來設定這個資料庫物件。

說到關閉資料庫部分,LevelDB只需要請求刪除指標就行了,但SQLite3必須呼叫關閉函式。Kyoto Cabinet 和BerkeleyDB的資料庫物件自身有一個close()方法。

我相信像LevelDB 和SQLite3那樣強制使用資料庫物件的指標,然後將指標傳遞給開啟函式是很“C風格”的。另外,我認為LevelDB處理關閉的方法—通過刪除指標—是一個設計缺陷。因為這會導致API的不對稱。在API中,函式的對稱應該儘可能的對稱,因為這樣更加直觀和邏輯。“如果我呼叫了open() 那我就應該呼叫close()”的想法比“如果我呼叫了open() 那我就應該刪除指標”的想法合乎邏輯一萬倍。

設計決策

因此我決定使用在KingDB上的是類似於Kyoto Cabinet 和Berkeley DB的,先例項化一個資料庫物件,然後呼叫物件的Open() 和Close()方法。至於命名,我仍使用傳統的Open() 和Close()。

 

3.2 讀寫

在本節,我比較他們讀寫功能的API。

我不會考慮SQLite3的設計,因為其是基於SQL的,因此其讀寫是通過SQL請求進行的,而非方法呼叫。Berkeley DB請求Dbt類物件的建立,並在上面進行一大堆設定,因此我也不會考慮這個設計。剩下的只有LevelDB 和Kyoto Cabinet,而他們有很漂亮的getter/setter對稱介面。LevelDB 有Get() 和Put(), 而Kyoto Cabinet 有get() 和set()。Setter方法的原型——Put() 和set()十分相似:鍵名是值傳遞,而鍵值是傳遞的指標使得呼叫時可以更改。鍵值並不通過呼叫返回,返回值是給錯誤管理使用的。

設計決策

對於KingDB,我打算使用和LevelDB 及Kyoto Cabinet相似的方法,對於setter方法使用一個相似的原型,即用值傳遞鍵值而用指標傳遞鍵值。至於命名,一開始我覺得Get() 和Set()是最好的選擇,但仔細思考之後我更傾向於LevelDB那樣,使用Get() 和Put()。其原因是Get/Set 和Get/Put都很對稱,但“Get” 和 “Set”兩個詞太相似,只差了一個字母。因此閱讀程式碼的時候使用“Get” 和“Put”會更加清晰且更易辨認,因此我會使用Get/Put。

3.3 遍歷

在上一節中,SQLite3不被考慮是因為其不滿足鍵值對儲存的需求。但看看它是如何將一個SELECT請求傳送到資料庫,然後在取回來的每一行上呼叫回撥函式是比較有趣的。大多數MySQL 和 PostgreSQL的API用迴圈並呼叫一個能夠填充本地變數的函式來做到,而非這樣使用一個回撥函式。我發現這種回撥函式比較棘手,因為這對於那些想執行合計操作或對取回來的行進行計算的使用者來說,會讓事情變得複雜。但這是另一方面的討論,現在回到我們的鍵值對儲存上來!

這裡有兩種方法:使用遊標或者使用遍歷器。Kyoto Cabinet 和BerkeleyDB使用遊標,一開始建立一個指向遊標物件的指標並例項化物件,然後在while迴圈中重複呼叫遊標的get()方法來獲取資料庫中所有的值。LevelDB使用遍歷器設計模式,一開始建立一個指向遍歷器物件的指標並例項化物件(這部分和遊標一樣),但是使用一個for迴圈來遍歷集合中的專案。注意這裡的while和for迴圈只是習慣:遊標可以使用for迴圈而遍歷器也可以使用while迴圈。其主要的不同是,在遊標中,鍵和值是指標傳遞然後在遊標的get()方法中填充內容,但在迭代器中,鍵和值是通過迭代器方法的返回值來訪問的。

設計決策

同樣,遊標和其while迴圈是相當“C風格”的。我發現迭代器的方法更加清晰並更符合“C++風格”,因為這正是C++中STL的集合的訪問方式。因此對於KingDB來說,我選擇使用LevelDB那樣的遍歷器。至於命名,我簡單的複製了LevelDB中的方法名。

3.4 引數處理

引數在IKVS系列文章中第三部分3.4節已經簡要敘述了,但我還想在這提一下。

SQLite3是通過sqlite3_config()修改全域性引數,然後在所有後續連線建立的時候應用。Kyoto Cabinet 和Berkeley DB中,選項是在呼叫open()之前通過呼叫資料庫物件的方法來設定選項的,和SQlite3的做法比較相似。在這些方法之上,更通用的選項是通過open()方法的引數來設定的(見上文3.1節)。這表示選項被分為兩部分,一些通過方法的呼叫來設定,而另一些是通過open()的呼叫來設定。

LevelDB的做法不大一樣。選項是在自己的類中一起定義,而引數是通過這些類的屬性來更改。之後這些設定類的物件以方法引數的形式傳遞,並總是第一個引數。例如LevelDB資料物件的open()方法的第一個引數是leveldb::Options類的物件,而Get()和Put()方法的第一個引數分別是leveldb::ReadOptions 和leveldb::WriteOptions。這種設計的一個好處是在同時建立多個資料庫的情況下可以很簡單的共享設定,儘管在Kyoto Cabinet 和 Berkeley DB的例子中可以為一組設定建立一個方法,然後通過呼叫這個方法來設定這組設定。像LevelDB那樣把設定放到一個特定的類中真正的優勢在於,其介面更穩定,因為擴充套件設定只需要修改這個選項類,而不用修改資料庫物件的任何方法。

儘管我想用這種選項類,但我必須說的是LevelDB這種總是將選項作為第一個引數在各個方法中傳遞的方式我不是很習慣。如果沒有需要修改的選項,這導致程式碼中需要使用預設選項,就像這樣:

這可能導致程式碼膨脹,而另一種可能是將選項作為最後一個引數,然後為這個引數設定一個預設值,使得不需要設定選項的時候可以省掉這項。而另一種源自於C++的解決方式是函式的過載,有數個帶有原型的方法使其可以省略掉選項的物件。把選項放到引數的最後對於我來說看上去更符合邏輯,因為其是可能省略的。但我相信LevelDB的作者把選項作為第一個引數是有很好的原因的。

設計決策

對於引數處理,我覺得將選項作為類是最簡潔的方式,同時其符合物件導向設計。

對於KingDB來說,我會像LevelDB那樣使用獨立的類來處理選項,不過我會將作為方法的最後一個引數。我或許以後能明白將選項作為最後一個引數是真正正確的方法——或者有誰能幫我解釋下——但現在我堅持將其放到最後。最後,命名子啊這兒不是很重要,因此Options, ReadOption 和WriteOption都可以。

3.5 錯誤管理

在IKVS系列第三部分3.6節,有關於錯誤管理的一些討論,基本上是說使用者看不到的程式碼是如何管理錯誤的。本節再次討論這個話題但稍有不同,不討論庫中錯誤的細節,而是關於錯誤發生後是怎麼報告給使用公共介面的使用者的。

Kyoto Cabinet, Berkeley DB 和SQLite3使用相同的方法處理錯誤,即其方法返回一個整型的錯誤程式碼。如在IKVS系列第三部分3.6節所述,Kyoto Cabinet內部將值設定在資料庫物件中,這就是為何上述示例程式碼中,錯誤資訊是從db.error().name()取出的。

LevelDB有個一特別的Status類,包含錯誤型別和提供了關於此錯誤更多資訊的訊息。LevelDB庫中的所有方法都返回了此類的一個物件,這使錯誤測試和將錯誤傳遞給系統各部分以進行進一步的檢查更加簡單。

設計決策

返回錯誤程式碼而避免使用C++的異常處理機制是十分正確的,然而整形並不足以攜帶有意義的資訊。Kyoto Cabinet, Berkeley DB 和SQLite3都有其自己的儲存錯誤資訊的方法,然而即便是在在Kyoto Cabinet 和Berkeley例子中,建立了錯誤管理和資料庫類的強耦合,,仍然會為取得資訊新增額外的步驟。像LevelDB那樣使用一個Status類可以避免使用C++異常處理,同時也避免了和架構其他部分的耦合。

4.結論

API的預設比較有意思,因為去看不同的工程師如何解決相同的問題總是很有意思的。這同樣讓我意識到Kyoto Cabinet 和Berkeley DB的API有多麼相似。Kyoto Cabinet 的作者Mikio Hirabayashi清楚地宣告瞭他的鍵值對儲存是基於Berkeley DB的,而在看完API相似性之後這一點更加清晰了。

LevelDB的設計相當好,但我還是對於一些我認為可以以其他方式實現的細節有些意見。例如資料庫開啟和關閉以及方法原型。

我吸取了每個系統的一點長處,而我現在對於KingDB的API設計的各個選擇感覺更加自信了。

 

5.參考文獻

[1] http://www.infoq.com/presentations/effective-api-design
[2] http://www.infoq.com/articles/API-Design-Joshua-Bloch
[3] http://www.amazon.com/Effective-Specific-Improve-Programs-Designs/dp/0321334876
[4] http://www.amazon.com/Effective-Java-Edition-Joshua-Bloch/dp/0321356683
[5] http://fallabs.com/kyotocabinet/spex.html
[6] http://leveldb.googlecode.com/svn/trunk/doc/index.html
[7] http://docs.oracle.com/cd/E17076_02/html/gsg/CXX/index.html
[8] http://www.sqlite.org/quickstart.html

相關文章