關於API的設計與實現

http://blog.csdn.net/horkychen/article/details/46612899php

API的設計是軟件開發中一個獨特的領域。最主要的特殊點在於API是供開發者使用的界面,即Application Programmer Interfaces。相似於用戶能夠直接使用到的GUI的做用同樣。因此相對於依據軟件設計的原則,考慮用戶的」體驗」會更加劇要。程序員

許多著名的工具和庫的做者都寫過相關的著做,詳細的論述他們在API上的設計與實現要點。下面的論述,就是從這些前人的工做成果中總結而來。如下先列出參考資料:算法

關於API

狹義上API可能只是一個動態庫(共享庫)提供功能的接口定義。廣義上API分爲public API,以及internal API之分。既有總體軟件系統對外輸出的接口(包括與設備通信的接口),也有系統內一個底層模塊提供給上層模塊使用的接口定義。編程

API看似簡單的名詞,卻表明着重要的架構設計。從架構設計的角度來看(所謂的組成論),軟件系統就是模塊和接口。模塊(層次/組件)決定分工,接口決定交互。API就是接口的定義。模塊間並不須要關心其它模塊的實現,只須要了解如何進行協做便可。這樣將複雜度分散到各個模塊之中,使得總體系統更爲可控。而API的本質,就是提供給模塊開發者使用的接口,是給」人(Programmer)」用的。API的設計任務的核心就是保證使用者以較低的成本,正確的使用接口,驅動模塊完成他們的業務。對於Public API,最大的設計挑戰則是如何把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

相關文章
相關標籤/搜索