在奇趣(Trolltech),爲了改進Qt的開發體驗,咱們作了大量的研究。這篇文章裏,我打算分享一些咱們的發現,以及一些咱們在設計Qt4時用到的原則,而且展現如何把這些原則應用到你的代碼裏。html
優秀API的六個特性程序員
便利陷阱算法
布爾參數陷阱編程
靜態多態windows
命名的藝術安全
指針仍是引用?數據結構
案例分析:QProgressBar框架
如何把API設計好ide
設計應用程序接口(APIs)是有難度的,這是一門和設計語言一樣難的藝術。要遵循許多不一樣的原則,這些原則中的許多還彼此衝突。函數
如今,計算機科學教育把很大的力氣放在算法和數據結構上,而不多關注設計語言和框架背後的原則。這讓應用程序員徹底沒有準備去面對愈來愈重要的任務:創造可重用的組件。
在面嚮對象語言普及以前,可重用的通用代碼大部分是由庫提供者寫的,而不是應用程序員。在Qt的世界裏,這種情況有了明顯的改善。在任什麼時候候,用Qt編程 就是寫新的組件。一個典型的Qt應用程序至少都會有幾個在程序中反覆使用的自定義組件。通常來講,一樣的組件會成爲其餘應用程序的一部分。KDE,K桌面環境,走得更遠,用許多追加的庫來擴展Qt,實現了數百個附加類。
可是一個優秀,高效的C++ API到底是怎樣的呢?它的好壞取決於許多因素,好比說,手頭上的任務和特定目標羣體。優秀的API具備不少特性,它們中的一些是廣泛所要指望的,另外一些是針對特定問題領域的。
優秀API的六個特性
API對於程序員就至關於GUI對於最終用戶。API中「P」表明程序員(Programmer),而不是程序(Program),強調這一點是爲了說明API是讓程序員使用的,程序員是人而不機器。
咱們認爲APIs應當精簡而完備,具備清晰簡單的語義,直觀、易記且應使代碼具備可讀性。
精簡性:精簡的API具備儘量少的類和公共成員。這使得理解,記憶,調試,更改API更加容易。
完備性:是指要提供全部指望的功能。這可能與精簡性原則相沖突。還有,若是成員函數放在不相匹配的類中,那麼許多要使用這個功能函數的潛在用戶會找不到它。
擁有清晰且簡單的語義:就像其餘設計工做同樣,你必須遵照最小驚奇原則(the principle of least surprise)。讓常見的任務簡單易行。不常見的工做可行,但不會讓用戶過度關注。解決特殊問題時,不要讓解決方案沒有必要的過分通用。(好比,Qt3中的QMimeSourceFactory能夠經過調用QImageLoader來實現不一樣的API。)
直觀性: 就像計算機上的其餘東西同樣,API應具備直觀性。不一樣的經驗和背景會致使對哪些是直觀,哪些不是直觀的不一樣見解。若是一箇中級用戶不讀文檔就可使用(a semi-experienced user gets away without reading the documentation),而且對並不知曉這個API的程序員來講,他可以理解使用了這個API的代碼,那麼這API就是具備直觀性的。
易於記憶:爲了讓API容易記憶,使用一致且精準的命名規範。使用容易識別的模式和概念,避免使用縮寫。
引向易讀的代碼(Lead to readable code):代碼只寫一遍,卻要閱讀許多遍(調試或更改)。易讀的代碼可能要多花點時間來寫,可是從產品生命週期中可節省不少時間。
最後,記住,不一樣類型的用戶會用到API的不一樣部分。雖然簡單的實例化一個Qt類是很是直觀的,可是指望用戶在嘗試子類化以前閱讀相關的文檔仍是合乎情理的。
便利陷阱
一般的誤讀是:越少的代碼越能使你達到編寫更好的API這一目的。請記住,代碼只寫一遍,卻要一遍又一遍地去理解閱讀它。好比:
QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");
遠比下面的代碼難讀(甚至難寫):
QSlider *slider = new QSlider(Qt::Vertical); slider->setRange(12, 18); slider->setPageStep(3); slider->setValue(13); slider->setObjectName("volume");
布爾參數陷阱
布爾參數經常致使難以閱讀的代碼。特別地,增長某個bool參數到現存的函數通常都會是個錯誤的決定。在Qt中,傳統的例子是repaint(),它帶有一個可選的布爾參數,來指定背景是否刪除(默認是刪除)。這就致使了代碼會像這樣子:
widget->repaint(false);
初學者很容易把這句話理解成「別重畫」!
這樣作是考慮到使用布爾參數能夠減小一個函數,避免代碼膨脹。事實上,這反而增長了代碼量。有多少Qt用戶真的記住了下面三行程序都是作什麼的?
widget->repaint(); widget->repaint(true); widget->repaint(false);
一個好一些的API可能看起來是這樣:
widget->repaint(); widget->repaintWithoutErasing();
在Qt 4中,咱們解決這個問題的辦法是,簡單地去除掉不刪除widget而進行重繪的可能性。Qt 4對雙重緩衝的原生支持,會使這功能被廢棄掉。
這裏還有一些例子:
widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding, true); textEdit->insert("Where's Waldo?", true, true, false); QRegExp rx("moc_*.c??", false, true);
一個顯而易見的解決方法是,使用枚舉類型代替布爾參數。這正是咱們在Qt4中QString大小寫敏感時的處理方法。比較下面兩個例子:
str.replace("%USER%", user, false); // Qt 3 str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4
靜態多態
類似的類應該有類似的API。在某種程度上,這能用繼承來實現,也就是運用運行時多態機制。可是多態也能夠發生在設計時期。好比,若是你用QListBox代替QComboBox,或者用QSlider代替QSpinBox,你會發現類似的API使這種替換很是容易。這就是咱們所說的「靜態多態」。
靜態多態也使API和程序模式更容易記憶。做爲結論,一組相關類使用類似的API,有時要比給每一個類提供完美的單獨API,要好。
命名的藝術
命名有時候是設計API中最重要的事情了。某個類應叫什麼名字,某個成員函數又應叫什麼名字,都須要好好思考。
通用的命名規則
有少量規則對全部類型的命名都適應。首先,正如我早先所提到的,不要用縮寫。甚至對用"prev"表明"previous"這樣明顯的縮寫也不會在長期中受益,由於用戶必須記住哪些名字是縮寫。
若是API自己不一致,事情天然會變得很糟糕,好比, Qt3有activatePreviousWindow()和fetchPrev()。堅持「沒有縮寫」的規則更容易建立一致的API。
另外一個重要但更加微妙的規則是,在設計類的時候,必須盡力保證子類命名空間的乾淨。在Qt3裏,沒有很好的遵照這個規則。好比,拿QToolButton來舉例。若是你在Qt3裏,對一個QToolButton調用name()、caption()、text()或者textLabel(),你但願作什麼呢?你能夠在Qt Designer裏拿QToolButton試試:
name屬性繼承自QObject,表示一個對象用於除錯和測試的內部名字。
caption屬性繼承自QWidget,表示窗口的標題,這個標題在視覺上對QToolButton沒有任何意義,由於他們老是跟隨父窗口而建立。
text屬性繼承自QButton,通常狀況下是按鈕上現實的文字,除非useTextLabel爲真。
textLabel在QToolButton裏聲明,而且在useTextLabel爲真時顯示在按鈕上。
因爲對可讀性的關注,name在Qt4裏被稱做objectName,caption變成了windowsTitle,而在QToolButton裏再也不有單獨的textLabel屬性。
給類命名
標識一組類而不是單獨給每一個類找個恰當的名字。好比,Qt4裏全部模式感知項目的視圖類(model-aware item view classes)都擁有-View的後綴(QListView、QTableView和QTreeView),而且對基於部件的類都用後綴-Widget代替(QListWidget、QTableWidget和QTreeWidget)。
枚舉類型和值類型命名
當設計枚舉時,咱們應當記住C++中(不像Java或C#),枚舉值在使用時不帶類型名。下面的例子說明了對枚舉值取太通常化的名字的危害:
namespace Qt { enum Corner { TopLeft, BottomRight, ... }; enum CaseSensitivity { Insensitive, Sensitive }; ... }; tabWidget->setCornerWidget(widget, Qt::TopLeft); str.indexOf("$(QTDIR)", Qt::Insensitive);
在最後一行,Insensitive是什麼意思?一個用於命名枚舉值的指導思想是,在每一個枚舉值裏,至少重複一個枚舉類型名中的元素:
namespace Qt { enum Corner { TopLeftCorner, BottomRightCorner, ... }; enum CaseSensitivity { CaseInsensitive, CaseSensitive }; ... }; tabWidget->setCornerWidget(widget, Qt::TopLeftCorner); str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
當枚舉值能夠用「或」鏈接起來看成一個標誌時,傳統的作法是將「或」的結果做爲一個int保存,這不是類型安全的。Qt4提供了一個模板類 QFlags<T>來實現類型安全,其中T是個枚舉類型。爲了方便使用,Qt爲不少標誌類名提供了typedef,因此你可使用類型 Qt::Alignment代替QFlags<Qt::AlignmentFlag>。
爲了方便,咱們給枚舉類型單數的名字(這樣表示枚舉值一次只能有一個標誌),而「標誌」則使用複數名字。好比:
enum RectangleEdge { LeftEdge, RightEdge, ... }; typedef QFlags<RectangleEdge> RectangleEdges;
在某些狀況下,"flags"類型有單數形式的名稱。在這種狀況下,枚舉類型以Flag後綴標識:
enum AlignmentFlag { AlignLeft, AlignTop, ... }; typedef QFlags<AlignmentFlag> Alignment;
函數和參數的命名
給函數命名的一個規則是,名字要明確體現出這個函數是否有反作用。在Qt3,常數函數QString::simplifyWhiteSpace()違反了這個原則,由於它返回類一個QString實例,而不是像名字所提示的那樣,更改了調用這個函數的實例自己。在Qt4,這個函數被重命名爲QString::simplified()。
參數名對於程序員來講是重要的信息來源,即便它們不出如今調用API的代碼中.既然現代的IDE會在程序員編碼時顯示這些參數,因此很是值得在頭文件中給這些參數取恰當的名字,而且在文檔中也使用相同的名字。
給布爾值設置函數(Setter)、提取函數(Getter)和屬性命名
給布爾屬性的設置函數和提取函數取一個合適的名字,老是特別困難的。提取函數應該叫作checked()仍是isChecked()?scrollBarsEnabled()仍是areScrollBarEnabled()?
在Qt4裏,咱們使用下列規則命名提取函數:
(1)形容類的屬性使用is-前綴。好比:
isChecked()
isDown()
isEmpty()
isMovingEnable()
另外,應用到複數名詞的形容類屬性沒有前綴:
scrollBarsEnabled(),而不是areScrollBarsEnabled()
(2)動詞類的屬性不使用前綴,且不使用第三人稱(-s):
acceptDrops(),而不是acceptsDrops()
allColumnsShowFocus()
(3)名詞類的屬性,一般沒有前綴:
autoCompletion(),而不是isAutoCompletion()
boundaryChecking()
有時,沒有前綴就會引發誤解,這種狀況使用前綴is-:
isOpenGLAvailable(),而不是openGL()
isDialog(),而不是dialog()
(若是函數叫作dialog(),咱們一般會認定它會返回QDialog*類型)
設置函數名字能夠從提取函數名推出來,只是移掉了全部前綴,並使用set-作前綴,好比:setDown()還有setScrollBarsEnabled()。屬性的名字與提取函數相同,只是去掉了「is」前綴。
指針仍是引用?
傳出參數的最佳選擇是什麼,指針仍是引用?
void getHsv(int *h, int *s, int *v) const void getHsv(int &h, int &s, int &v) const
大部分C++書推薦在能用引用的地方就用引用,這是由於通常認爲引用比指針更「安全且好用」。然而,在奇趣(Trolltech),咱們傾向使用指針,由於這讓代碼更易讀。比較:
color.getHsv(&h, &s, &v); color.getHsv(h, s, v);
只有第一行能清楚的說明,在函數調用後,h、s和v將有很大概率被改動。
案例分析:QProgressBar
爲了在實際代碼中說明這些概念,咱們以QProgressBar在Qt3和Qt4中的比較進行研究。在Qt 3中:
class QProgressBar : public QWidget { ... public: int totalSteps() const; int progress() const; const QString &progressString() const; bool percentageVisible() const; void setPercentageVisible(bool); void setCenterIndicator(bool on); bool centerIndicator() const; void setIndicatorFollowsStyle(bool); bool indicatorFollowsStyle() const; public slots: void reset(); virtual void setTotalSteps(int totalSteps); virtual void setProgress(int progress); void setProgress(int progress, int totalSteps); protected: virtual bool setIndicator(QString &progressStr, int progress, int totalSteps); ... };
API至關複雜,且不統一。好比,僅從名字reset()並不能理解其做用,setTotalSteps()和setProgress()是緊耦合的。
改進API的關鍵,是注意到QProgressBar和Qt4的QAbstractSpinBox類及其子類QSpinBox,QSlider和QDial很類似。解決方法?用minimum、maximum和value代替progress和totalSteps。加入alueChanged()信號。加入setRange()函數。
以後觀察progressString、percentage和indicator實際都指一個東西:在進度條上顯示的文字。通常來講文字是百分比信息,可是也可使用setIndicator()設爲任意字符。下面是新的API:
virtual QString text() const; void setTextVisible(bool visible); bool isTextVisible() const;
默認的文字信息是百分比信息。文字信息能夠藉由從新實現text()而改變。
在Qt3 API中,setCenterIndicator()和setIndicatorFollowStyle()是兩個影響對齊的函數。他們能夠方便的由一個函數實現,setAlignment():
void setAlignment(Qt::Alignment alignment);
若是程序員不調用setAlignment(),對齊方式基於當前的風格。對於基於Motif的風格,文字將居中顯示;對其餘風格,文字將靠在右邊。
這是改進後的QProgressBar API:
class QProgressBar : public QWidget { ... public: void setMinimum(int minimum); int minimum() const; void setMaximum(int maximum); int maximum() const; void setRange(int minimum, int maximum); int value() const; virtual QString text() const; void setTextVisible(bool visible); bool isTextVisible() const; Qt::Alignment alignment() const; void setAlignment(Qt::Alignment alignment); public slots: void reset(); void setValue(int value); signals: void valueChanged(int value); ... };
如何把API設計好
API須要質量保證。第一個修訂版不多是正確的;你必須作測試。寫些用例:看看那些使用了這些API的代碼,並驗證代碼是否易讀。
其餘的技巧包括讓別人分別在有文檔和沒有文檔的狀況下,使用這些API而且爲API類寫文檔(包括類的概述和獨立的函數)。
當你卡住時,寫文檔也是一種得到好名字的方法:僅僅是嘗試把條目(類,函數,枚舉值,等等呢個)寫下來而且使用你寫的第一句話做爲靈感。若是你不能找到一 個精確的名字,這經常說明這個條目不該該存在。若是全部前面的事情都失敗了而且你確認這個概念的存在,發明一個新名字。畢竟,「widget」、 「event」、「focus」和「buddy」這些名字就是這麼來的。