設計流程
設計一套API可能要幾人年的工作量。設計的每一步都是完善的過程,當然也可能是搞砸API的過程。以下的原則可能有助于你更好地設計API。
仔細研究需求
設計之前要仔細研究需求,知道需要的是什麼。多咨詢大家,比如你老闆同僚和使用者,看他們想要怎樣的功能。
Qt4.3的MDI實作之前就在内部開發郵件清單上征求了很多人的意見。大家對以前的MDI架構存在的問題和沒有的功能進行了深入的讨論,對API設計的幫助很大。
設計之前先寫用例
一般設計API的通病是先實作功能,然後設計API,最後釋出。其實應該先設計再實作。
開始設計API之前,先寫幾個使用這套API的代碼片段。在這個階段先不要考慮實作的難度。用例寫完,API的雛形也就出來了。總的原則是“讓事情簡化,不可能變為可能”。
一個例子就是QWizard。QWizard有兩種,一種是簡單的線性Wizard,不能跳來跳去的,另一種是複雜的。經過使用用例我們發現簡單的Wizard可以看作是複雜Wizard的一個特例,這樣就簡化了API設計。
研究同一類庫中類似的API設計
要設計XmlQuery,一個好的辦法就是參考本類庫中的SqlQuery。這兩個概念很相似,都是完成查詢,浏覽結果,顯示等。熟悉SqlQuery的使用者不用費勁就能學會新的API。你也可以參考SqlQuery的構架方式,減輕設計工作量。
當然,完全照抄也是很傻的。你應該批判地繼承,加以發展。首先完善這個設計,然後加以學習。
如果要給一套API寫新版本,第一件事就是要透徹地了解這套API。不要全盤否定舊的設計,不要試圖代替,而要創造性地設計。為了相容的需要,你可能要包含所有上一版本的功能。
腦殘的例子:Qt4.0中,QDockWindow被改名為QDockWidget,沒有任何原因;QTextEdit::setOverwriteMode()被取消了,後來4.1又重新加入。
先設計,後實作
實作API之前,要确定API的文法。對于使用者很多的庫,認可你自己的實作麻煩複雜一些,也要讓使用者用着簡單直接。
Qt4中,可以先生成一個QWidget,以後再設定它的parent。在4.0和4.1中,這将會在背景創造一個視窗句柄,開銷很大。在4.2 中,實作了 delayed window creation,解決了這個問題,這是一個很好的API驅動設計的例子。在Qt3裡,這個問題是通過QWidget::recreate()解決的。這個API就是純粹為了實作而實作的。
要記住,API和它的文法才是庫提供的最終産品。很多産品的實作變了多次,但是API設計始終如初,如UNIX/POSIX, OpenGL, QFileDialog。
實作API的過程中,要不斷寫unit test。這樣你才能發現很多漏洞和空白,然後細化你的設計。但是不要讓實作細節過多影響API的設計,除非是為了一些特殊的原因如性能。
QGraphicsScene::setBspTreeDepth() 就是這樣一個例子。這個API純粹是為了提高性能。使用者控制BSP樹的深度可以提高性能,但是大多數情況下,系統預設的樹深度也可以滿足性能需要。是以這個API用了一個比較專業的詞Bsp,表明了這是深入到API實作内部的一個進階API。初哥一看這個詞不認識就不會輕易嘗試了。
找人幫你評測API
你應該像孫子一樣乞求别人多給你的API一些評測意見,特别是負面的。這些意見更能幫助你改進設計。
多寫幾個例子程式
設計好API後,一定要寫幾個例子。你可以使用設計之前寫的用例。如果有人能幫你寫這些例子程式那就更好了。
Qt所帶的Class Wizard和License Wizard例子都來自于設計用例。
做好擴充的準備
有兩類人會擴充你的API:API維護者:他們會增減你的API接口;使用者:他們會通過定制和繼承來豐富你API的功能。
擴充性設計要仔細分析實際的目标。對于那些有虛函數的類,至少要試着寫3個子類來驗證這些API實作了所有需要的功能,這個我們一般叫作“3個原則”。
在設計Qt4.0的時候,QAbstractSocket設計得就不怎麼好。Qt4.3要加入QSSLSocket的時候,我們不得不手工降格其中好幾個API,因為他們沒被設計成虛函數。好在它們是在同一個庫中,可以用“手工多态”解決,否則悲劇就無法避免。
内部API沒評測之前不要釋出
有些API一開始是内部使用的,後來大家覺得很有用,才公開釋出。一個常見的錯誤就是釋出之前沒有進行完整的測試。比如Qt就曾經釋出過帶有拼寫錯誤的API,不堪回首。
甯缺毋濫
如果對API的功能不是很确定,萬萬不要釋出,甯可暫時當作内部API,或者日後再說。
使用者的回報很重要,但是實作使用者所期待的所有功能是不可能的。一般等3個客戶要求同樣的功能後再實作是比較明智的。
設計原則
這裡羅列了一些API設計的基本原則,大部分都來自實際的API設計經驗。其中有些看似沖突,但是其實都有道理。掌握尺度的是你自己,沒有什麼能替代你自己的思考,原則隻是原則而已。
命名
名字要能解釋自己,要遵從英語文法。QPainterPath的作者建議,在文檔裡把它叫做vector path,因為這是大家通用的叫法。另一個例子是MDI,盡管實作的是MDI,在Qt4.2之前卻叫做QWorkspace。在4.3之後,改為了QMdiArea。
另外,參數的命名也要清楚明白。盡量少用bool類型的參數,這樣的代碼不好讀。QWidget::repaint()就帶了一個bool類型的參數,來訓示是否在重畫之前擦除背景。如果有repaint(false)這樣的代碼,就很容易讓人誤會,到底是不是不要repaint還是怎樣?解決的方法之一就是用枚舉代替bool,如
repaint(QWidget::eraseBackground);
命名要統一。不要混用類似widget和control這類詞語,這會讓使用者亂猜。參數的順序也要一緻,比如畫方框的函數參數為(x,y,width,height),别的地方也要類似,不要弄成(x,width,y,height)。
比如QStackArray,是一種變長的數組。由于用了stack這個詞,很容易和QStack混淆。4.1之後,這個類被改為QVarLengthArray。
了解你的使用者也很重要。比如你實作了一套關于XML的API,名字裡帶有XML就是一個很好的主意。如果你自認為API很高檔,一定要叫做什麼IDREFs或者NCNames,使用者會很讨厭的。
命名是API設計的一項重要内容。你設計的名字可能會出現在一些IDE的自動完成功能中,這些名字和參數名必須意義明确,簡短有力。尤其要避免一個字母長度的參數名。
避免二義性
一個名字要嚴格對應一個概念。假如你有兩種事件傳遞機制,一個是同步的,一個是異步的,分别叫做sendEventNow()和 sendEventLater()就不錯。如果使用者必須了解同步異步概念,你也可以叫做sendEventSynchronously()和 sendEventAsynchronously()。
如果你要鼓勵使用者多用同步方式,可能會把同步的方法改為sendEvent()。如果你希望使用者用異步方式,就可以反過來把異步方法命名為sendEvent()。
Qt中的sendEvent()是同步的,postEvent()是異步的。這裡就利用了英語中send和post的微妙語義差别。
在命名複制初始化函數的參數時尤其要注意。下列代碼:
Car &Car::operator=(const Car &car) { m_model=car.m_model; m_year=car.m_year; ... return *this; }
這段代碼很不好,兩個car很容易混淆。
注意完整性
API設計跟寫書一樣,要注意對稱和前後照應。格式盡量一樣,過程盡量一樣,這樣讀者能更容易了解你的意圖。比如所有的set函數都用set開頭,這樣使用者更容易習慣。
在 Qt3中,有一個函數QStatusBar::message(text,msecs)能在狀态條上顯示一條資訊msecs毫秒。但這個函數怎麼看都像一個get函數。Qt4中,我們曾考慮改名為setMessage()以達到一緻性。但是setMessage有兩個參數,不太像set函數,最終我們決定改名為showMessage(),以便區分。
再看event那個例子。同步的時候,可以把event對象當作參數傳遞,因為馬上就會傳回,函數可以直接删除局部變量。但是異步時,就要建立一個新對象,完成以後删掉,否則就會有記憶體洩露。是以我們應該把兩個API分别設計成:
sendEventNow(Event event); sendEventLater(Event* event);
以避免使用者亂用。很不幸地,Qt在這裡犯了腦殘的錯誤,sendEvent和postEvent都是接受Event*的參數,這就很容易造成記憶體洩露。當然,為了一緻,你可以定義兩個都接受Event*,然後自己管理event對象的生存期,這樣效率很低下但是很安全。有時候我們就是要在平衡之間作出選擇。
别用縮寫
盡量避免縮寫。當然有一些常見的例外,如min,max,dir,rect,prev。但是要注意有一緻性,不能有的用有的不用。Qt本身在這方面做得其實相當不好。對于參數命名來說,可以适當放寬限制,但是也要保證意思清楚明晰。
名字要專不要通
API的名字空間是很寶貴的。盡量用專用名,否則一旦通用的名字被用了,以後就很難有機會收回來。QRegExp其實被叫做QStringPattern也很恰當,但是這個名字太通用了,是以最後還是選擇了QRegExp。
比如你要給SQL添加一個錯誤報告類,最好叫做SqlErrorHandler而不是ErrorHandler,否則将來很難與XmlErrorHandler作出區分。将來擴充庫的時候,如果要用到ErrorHandler作為基類也不會頭疼。
Qt在某些方面做得也很不好。比如QDom系列類,就沒有區分SAX和DOM的分支,這造成了一定的混亂。
不要太過遷就下層API
如果你要包裝一系列API,不要被它的命名方式所支配。按照你自己的命名規則統一命名方式。你設計的目的是讓使用者使用友善高效,而不是遷就下層的庫。
選擇合适的預設值
在Qt中設計一個按鈕很容易:
QPushButton * button=new QPushButton(text,paret);
如果你編過Cocoa程式,你就知道,要生成一個按鈕要設定9個參數,而99%的時間你選擇的初始參數都是一樣的。為什麼不用預設值呢?這就是Qt聰明的地方。盡量讓你的客戶省事,猜測他們需要什麼預設值,不要讓他們費勁,隐藏不必要的細節,這就是API的設計之道。
通過選擇合适的預設值,不僅可以減少代碼量,還可以讓API簡單可預測。尤其當你有bool類型參數的時候,盡量讓預設值為false。不要以為參數越多API就越強大,你需要的是易用的API。
不要自作聰明
API應該簡單清楚,盡量少讓使用者産生驚訝的感覺。如果過于自作聰明把API弄得不易用,就遠離了API的本來目的。盡量貼近你使用者的習慣而不是試圖教他們怎麼做,否則你就等着寫文檔去吧。
Qt3 的QLabel就是一個自作聰明的例子。QLabel::setText()內建了顯示普通文本和html文本的功能。貌似節省了一個API,但是這樣很容易被客戶誤用。如果客戶想顯示一些html的源代碼,還必須調用setTextFormat(),大部分人并不知道這個進而變得無所适從。避免自作聰明的方法是分開兩個setText()和setHtml()。
注意邊界值
對于類庫來說,邊界值的處理相當重要,認為邊界值發生機率很小就不加注意是很幼稚的。邊界值造成的問題會在使用這個類的其他類中得到擴散和放大。比如字元串查找函數有邊界值問題,在正規表達式中,這個問題很可能就會被放大。
處理邊界值的一個常見錯誤是在函數開始的時候就加入邊界檢查。這樣做大多數時候并不是必要的。建議你先按照正常的情況進行處理,最後才對邊界值進行處理,這樣可以提高API的效率。另外就是要記得在unit test中加入邊界值的測試。
小心定義虛API
虛API一般更難定義,并且很容易在新版本釋出時出錯。這個問題叫做“fragile base class problem”。設計虛API時,要注意以下兩個問題:
第一是定義的虛API太少,以後發現不夠用。一開始很難知道将來要用什麼樣的API,要用多少。萬一定義的API不夠用,會限制使用者的擴充功能。
第二個就是濫用virtual。在C++中,虛函數效率是很低的。如果你的類并不需要擴充這個功能,就不要定義成虛函數,否則不僅效率低下,還會誤導使用者。
設計API時,你必須全盤考慮,逐個過濾來決定哪些API應該是虛的,哪些不應該是,在文檔裡應該詳細說明你的類如何使用這些虛方法。
在C++裡,大部分虛函數應該被聲明為保護的,以保證不被錯誤修改調用影響其他類的通路。
Qt4 的QIODevice就是一個很好的例子。公用API為read(),write(),而虛函數為readData()和writeData()。這樣就避免了通路混亂的情況發生。QWidget也類似,公用API為show(),resize(),repaint(),而虛函數為 showEvent(),resizeEvent(),paintEvent()。
C++的一個很操蛋的地方就是加入虛函數肯定會破壞二進制相容。有一個很惡心的辦法可以避免這個問題,那就是定義一個通用的虛函數占位:
virtual void virtual_hook(int id, void * data);
結構性
很多API在建立對象的時候要求使用者指定一大堆的屬性,比如Win32程式設計:
m_hWindow = ::CreateWindow("AppWindow", /* class name */ m_pszTitle, /* title to window */ WS_OVERLAPPEDWINDOW, /* style */ CW_USEDEFAULT, /* start pos x */ CW_USEDEFAULT, /* start pos y */ m_nWidth, /* width */ m_nHeight, /* height */ NULL, /* parent HWND */ NULL, /* menu HANDLE */ hInstance, /* */ NULL); /* creatstruct param */
這麼多參數對于使用者來說是個噩夢。一般現代API會采用另外一種方式,就是基于屬性的設計。這樣使用者就可以用很多行代碼慢慢設計一個類執行個體,不需要幹預的非必須屬性完全可以不管。
window = new Window; window->setClassName("AppWindow"); window->setWindowTitle(winTitle); window->setStyle(Window::Overlapped); window->setSize(width, height); window->setModuleHandle(moduleHandle);
這樣做有多個優點:
* 看起來比較簡單
* 不用記住參數的順序
* 可讀性強,不需要特别說明注釋
* 屬性可以有預設值,不是必須指定所有的屬性
* 随時可以更改屬性
* 可以随時取得屬性,便于除錯
* 友善進行可視化圖形化設計
對于開發庫的牛人來說,對此要多多考慮一層。因為屬性設定的順序不确定,一般要進行”lazy initialization”來避免每一個屬性變化的時候都重新初始化整個對象。
比如 QRegExp,使用者可以這樣初始化:
QRegExp regExp("*.wk?", Qt::CaseInsensitive, QRegExp::Wildcard);
也可以這樣初始化:
QRegExp regExp; regExp.setPattern("*.wk?"); regExp.setCaseSensitivity(Qt::CaseInsensitive); regExp.setPatternSyntax(QRegExp::Wildcard);
在實作中,QRegExp把編譯表達式的過程延後到第一次使用時,避免了多次編譯。
最高境界是手中無劍
劍客的最高境界是手中無劍,心中有劍。最好的API是讓使用者完全不覺得在用你的API,而是在用他們最熟悉的工具,完全沒有障礙和隔閡。
在Qt3中,QWidget的最大限制是32768×32768。在Qt4中,已經沒有了這種限制。Qt4還增加了pdf格式的支援,StyleSheet支援,OpenGL支援。雖然這些功能很強大,但是API接口并沒有太大的變化,使用者體驗并沒有太多變化,也不用花費太多時間重新學習。在使用者不知不覺之間,新的功能,新的API已經進入了使用者的視野。使用者雖然渾然不知卻已不知不覺獲得了更加強大的工具而進入了程式設計的自由王國。什麼時候,你能讓使用者忘記API而快樂自然地使用你提供的功能時,你會發現自己已然是個API設計大師了。