API的設計與實現
關於API的設計與實現
API的設計是軟體開發中一個獨特的領域。最主要的特殊點在於API是供開發者使用的介面,即Application Programmer Interfaces。類似於使用者可以直接使用到的GUI的作用一樣。所以相對於依據軟體設計的原則,考慮使用者的”體驗”會更加重要。
許多著名的工具和庫的作者都寫過相關的著作,詳細的論述他們在API上的設計與實現要點。下面的論述,就是從這些前人的工作成果中總結而來。以下先列出參考資料:
- 1.軟體框架設計的藝術 (Jaroslav Tulach, NetBeans)
- 2.Little Manual of API Design (Jasmin Blanchete, Qt)
- 3.Preserving Backward Compatibility (Garrett Rooney, Subversion)
- 4.API Design Principles (Qt Wiki)
- 5.How to design a good API and Why it matters (Google)
關於API
狹義上API可能只是一個動態庫(共享庫)提供功能的介面定義。廣義上API分為public API,以及internal API之分。既有整體軟體系統對外輸出的介面(包括與裝置通訊的介面),也有系統內一個底層模組提供給上層模組使用的介面定義。
API看似簡單的名詞,卻代表著重要的架構設計。從架構設計的角度來看(所謂的組成論),軟體系統就是模組和介面。模組(層次/元件)決定分工,介面決定互動。API就是介面的定義。模組間並不需要關心其它模組的實現,只需要瞭解如何進行協作即可。這樣將複雜度分散到各個模組之中,使得整體系統更為可控。而API的本質,就是提供給模組開發者使用的介面,是給”人(Programmer)”用的。API的設計任務的核心就是保證使用者以較低的成本,正確的使用介面,驅動模組完成他們的業務。對於Public API,最大的設計挑戰則是如何把API一次就做對!
附1的作者在書中提到了一個”無緒(cluelessness)”的概念,即API的使用者不需要對API的內在邏輯有了解,可以只依據API的定義來使用API。更直白一點就是傻瓜式的API。
什麼是好的API
對於一般的開發任務,常常思考的是保證功能的正確性和設計的完美,可以不斷嘗試做創新和重構。但這些原則放到API設計上就不一定正確了,反而需要有些保守。先看一下KDE/Qt開發者總結出來的好API標準:
容易學習和記憶
(Easy to learn and memorize)
這包括了命名,模式的使用,最關鍵是對於經驗式程式設計的包容。所謂經驗式程式設計是指開發者常常不會認真讀完介面的文件(如果提供的話),而是根據思維的連續性,以過往的經驗來預先假定API的功能。比如,如果如下兩個類都有相同方法:
void Widget::SetSize(int width, int height);
void View::SetSize(int width, int height);
另一個類,邏輯上會自然的認為是View的子類,但卻提供如下的方法,就會讓人捉摸不透了:
void Button::Layout(int width, int height);
從經驗式程式設計的角度,使用Button::SetSize()是非常自然的事,程式設計師很可能不會認真核實這個Button竟然沒有提供這個方法。
作為API設計者,不能假定使用者都會認真的看完所有的文件,而是要儘量做到兩點:
- 保持與普遍認知一致的設計。
- 保持設計概念上的一致性(Consistency)。
那些被公認的行為和命名就非常重要,千萬不要做太多創新。請遵守最小驚喜原則。
簡潔清晰的語義
這樣有助於理解,也很難被誤用。當一個API無法滿足所有的需求時,不要嘗試為了一些極小場景來影響到一般的場景,可以另分一個獨立的路徑。這樣的情況,往往反應在函式的引數上。比如這樣的API(來自Win32), 你必須每次都要對著文件來呼叫了:
HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);
另外在附2裡舉了一個輸出如下HTML文字的例子:
the <b>goto <u>label</b></u> statement
以C++的實現可以為:
stream.writeCharacters("the ");
stream.writeStartElement("b");
stream.writeCharacters("goto ");
stream.writeStartElement("i");
stream.writeCharacters("label");
stream.writeEndElement("i");
stream.writeEndElement("b");
stream.writeCharacters(" statement");
很顯然,這裡Element的Start與End需要開發者自己處理。如果想要編譯器來幫助檢查,讓開發者少犯錯,則程式碼可以變為:
stream.write(Text("the ")
+ Element("b", Text("goto ") + Element("u", "label"))
+ Text(" statement"));
容易擴充套件及保證向後相容
之前的資料都是分散的談到兩者的,我將它們合併在這裡,因為它們都是API演變所必須考慮的。
隨著需求變化,API的演變是必須的,不可能存在一成不變的API。但是作為穩定的API則是對使用者的承諾,不單單是技術上。穩定的概念不是不變,而是指變化的成本要儘可能的低。
如果新增一個API會導致之前的程式碼無法編譯,或者程式無法正常執行,都會影響使用者對API的信任。
能夠鼓勵編寫可讀性程式碼
還是前面強調的,API是給程式設計師用的,所以本身的命名必須具備可讀性。同時,它還要設計成引導使用者寫出更具可讀性的程式碼。附2裡舉了如下的例子。
在Qt3中,Slider的建構函式允許使用者指定多個引數:
slider = new QSlider(8, 128, 1, 6, Qt::Vertical, 0, "volume");
而在Qt4,則需要這樣做:
slider = new QSlider(Qt::Vertical);
slider->setRange(8, 128);
slider->setValue(6);
slider->setObjectName("volume");
顯然後者更具可讀性。
這裡還是有爭議的。既不能為單獨的追求可讀性而將相關的東西分離開來,也不能為了簡化程式碼,而將不同的內容合在一起。
簡潔
這一點對於第一條特別重要。一個不斷膨脹,十分臃腫的API必然會產生各種理解和使用上困擾,特別是當多個API存在功能重疊的情況時。舉一個會帶來理解上困擾的例子:
void View::SetSize(int width, int height);
void View::SetWidth(int width);
void View::SetHeight(int height);
後兩者明顯是前者的兩個子任務,卻因為某些特別的原因被公開出來。就會出來到底是呼叫SetSize(),還是根據變化呼叫對應的SetWidth()或SetHeight()呢?
完整
如果需要提供的功能就要提供,一個介面類應當具備的函式(包括setters/getters)也應當在這個類中提供。
API的設計實現
關於API的設計實現,不同的背景,不同的需求會有不同的描述了。我這裡概括了一些他們間相通的要點。
工廠方法優於建構函式
如果公開一個建構函式,那麼建立的物件一定是類的例項。而工廠方法更具靈活性,雖然引數完全相同,但可以返回一個子類的例項。同時更利於實現單例或者快取物件例項。
在Chromium一些模組的介面上,常常可以看到這類的應用。
常量修飾符
常量修飾符,有助於限定不必要的修改動作,也是一種行為約定。無論是對引數,函式,或是返回值,都可以視需要新增常量修飾符。
基於屬性的API
相對於在建構時傳入一串引數的介面類,不如在建構後再以setter設定其它引數的方式。其區別在於後者更利於編寫可讀性的程式碼。在上面關於可讀性程式碼中已舉過例子,這裡不再贅述。
要點是各個屬性需要做到正交,且與順序無關。
Virtual APIs
對於是否需要提供虛擬函式形式的API,也是一直有爭論。這裡並不是討論介面類(純虛類)的定義,介面類的定義的必要性是明確的,不需要額外討論。
原則上對虛擬函式作為API是限制使用的,原因是繼承下的override可能會導致介面的行為變得不符預期,因為子類的行為無法確定。
但在一些場景下確實有必要為使用者提供一定的擴充套件性,就可以提供虛擬函式,以便使用者可以通過繼承改變原來的行為。
布林值引數
以整型資料代替Enum的作法類似,關鍵在於使用者的理解。
可以改進的做法包括,分成不同的函式實現,或者以列舉變數代替。
示例:
widget->repaint();
widget->repaint(true);
widget->repaint(false);
分開函式的方式:
widget->repaint();
widget->repaintWithoutErasing();
使用整數代替格列舉變數時也是相同的問題。
異常處理
在附5中作者詳細說明了關於API中的異常處理。我的總結是隻拋必須拋的異常,絕不能自作聰明的默默處理。API的程式碼應當最真實的反應出執行中的問題,更不能用聰明的程式碼做某些特別處理。其背後的原因是這樣做會使得API的行為與預期會發生偏差,違背了最小驚喜原則。
命名
在命名上,附2列舉的比較詳細。概括如下:
- 選擇具有自解釋能力的命名
核心是從使用者和領域的角度命名,而不是從自身的設計命名。比如Qt 4.2中QWorkspace實現了MDI (multiple document interface)。好在這樣的命名後來被修正為QMdiArea。 - 命名不要有歧義
如果遇到有概念相似的API,一定要從命名上將它們區分出來。如sendEvent()表示同步的事件,而sendEventLater()則表示非同步事件。 - 保持一致性
這一點對於前面對經驗式程式設計的支援很重要,也被稱為對稱性(Symmetry)。如果set字首代表的是setters,就不要出現以set打頭,但卻不是setter的情況。再比如Chromium中對setters/getters的定義以非常明確的方式獨立出來。 - 避免簡寫
簡寫除了是某種通用的縮寫外,不要隨意以首字母縮寫的形式定義簡寫。不然,讀者可能對名字完全不知所云。 - 優先使用特殊的命名,而不是通用的命名
一個通用的名字常常包含更為普遍的職責,如果API的功能帶有明確的應用場景,就應當在API上體現出來。否則一旦遇到需要一個通用API的情況,就用很多餘的加上XXXXInGeneral之類的命名,而且會讓使用者出現難以選擇適用API的情況。 - 不要太遷就於既有的命名
比如包裝一箇舊的或子功能的API的時候,常常會延用原有的API命名。其實完全沒必要,更合理的做法還是從新API的功能入手,選擇合適的名字。
關於向後相容
一個模組(庫)的相容性主要包括:
API相容
主要是定義上的相容性,即程式碼能否編譯,以及行為的一致性。ABI相容,即二進位制級的相容。
對於共享庫就是需要有相同的符號表,包括全域性的物件和定義。Linux裡這類問題太多了。通訊協議的相容
如果有自定義協議的網路通訊,就可能存在C/S之間通訊協議的相容性問題。儲存的資料及檔案格式的相容
如果使用者升級後,發現以前的歷史資料不可用了,大多數情況都是無法接受的,搞不好還要吃官司的。
保證相容性
至於要保證哪些點的相容性,取決於使用者的規模,以及影響的程度(或者使用者的承受能力)。從相容性的角度,保證相容性方法包括:
不要丟掉任何東西
非常悲催的現實。如果你棄用了API的某一部分(更不能改了),無論使用@Deprecated,還是在文件中反覆宣告,你都可能會造成使用者之前的程式碼失效。一定要保證之前API的完整性,除非你的相容性規則允許你放棄,就比如像MicroSoft一樣宣稱將不再支援某個版本。隱藏細節
可以使用Opaque Pointer (PIMPL)或者利用建構函式來幫助API隱藏內部的資料結構,而且讓使用者只能通過提供的函式來運算元據。保證協議及資料格式的擴充套件性
可以使用標準化的XML以及標準化的協議來取代自定義的格式。如果條件不允許,也記得在協議及資料格式中定義出版本,以便於後期做相容性處理。
預留欄位也是一個常用的做法。我曾經不止一次的遇到,通過協議中的預留欄位解決緊急問題的案例。實現上保證相容性
在實現邏輯上,特別是判斷處理也要注意相容性處理,這是一個常常犯錯的地方。以某個欄位flagA的處理為例:if (headers.flagA != 1) {
doB();
} else {
doA();
}
顯然將判斷條件改為headers.flagA == 1會讓實現更具相容性。否則,降級時,就是災難了。
極端的意見有害無益
(主要參考附1)
關於API定義的評價中,漂亮或者優雅都是很主觀的。我們應當設計易於使用,廣為接受且富有成效的API(節自附1)。至於所定義的原則,完合取決於API自身的需求。比如因為效能的原因,一些API可能無法滿足某些場景的需求,達不到完整性的要求。API的設計者不需要去滿足所有人,重要的是API本身保持正向的演進。比如標準的優化流程就比較適合API的發展:
1. Make it work
2. Make it right
3. Make everything work
4. Make everything right
5. ……
轉載請註明出處: http://blog.csdn.net/horkychen
進一步閱讀: 避免類的膨脹 (介面類適用)
相關文章
- Titan 的設計與實現
- LFU 的設計與實現
- Django RESTful API設計與實踐指南DjangoRESTAPI
- Picker元件的設計與實現元件
- RedisHttpSession 的設計與實現RedisHTTPSession
- 限流 SDK 的設計與實現
- Redis設計與實現Redis
- 《redis設計與實現》Redis
- Cobar SQL審計的設計與實現SQL
- Python實現火柴人的設計與實現Python
- 設計出色API的最佳實踐與原則 - JamesAPI
- 服務API版本控制設計與實踐API
- Java的API設計實踐JavaAPI
- RedisSyncer同步引擎的設計與實現Redis
- 數獨遊戲的設計與實現遊戲
- Cuckoo Filter:設計與實現Filter
- Android Binder設計與實現 - 設計篇Android
- 實現鍵值對儲存(四):API設計API
- Python實現微博輿情分析的設計與實現Python
- 淺析pplx庫的設計與實現。
- 旅遊網站的設計與實現網站
- 認證授權的設計與實現
- Steps 元件的設計與實現元件
- RocketMQ Compaction Topic的設計與實現MQ
- 聊聊「訂單」業務的設計與實現
- PouchContainer CRI的設計與實現方法AI
- Spring IOC容器的設計與實現Spring
- 事件匯流排的設計與實現事件
- linux核心設計與實現Linux
- 淺談VueUse設計與實現Vue
- OpenMP 原子指令設計與實現
- Redis 設計與實現 4:字典Redis
- Redis 設計與實現 (九)--LuaRedis
- [Hook] 跨程式 Binder設計與實現 - 設計篇Hook
- OpenStack設計與實現(二)Libvirt簡介與實現原理
- Redis 設計與實現 (五)--多機資料庫的實現Redis資料庫
- 綜合Twitter、Github等各大網站API設計經驗:RESTful API實用設計與最佳實踐 - Vinay SahniGithub網站APIREST
- 鬥魚 Juno 監控中心的設計與實現