"Designing Qt-Style C++ APIs" by Matthias Ettrich
http://doc.trolltech.com/qq/qq13-apis.html
翻譯這篇文章的目的不是讓人瞭解Qt,而是讓人試着學習點C++編程的軟技能。我從原文中得到的一些風格上的體會,也希望你能從中有所收穫.(譯者注)
我們在Trolltech做了大量研究來改進Qt開發體驗.在這篇文章中,我將分享我們的一些成果,呈現我們在進行Qt 4設計時所使遵循的原現,並向你展示如何將它們應用到你的代碼中.
設計應用程序接口(APIs)是有難度的.它是像跟設計編程語言一樣困難的藝術.要遵循許多不同的的原則,這些原則中的許多還彼此衝突.
現今的計算機教育過多關注於算法和數據結構,很少去關注隱藏在程序設計語言和程序框架後面的那些設計原則.這使得程序員們面對日益重要的任務,創建可複用的組件,毫無準備.
在面嚮對象語言出現前,通用的可複用的代碼大都由庫提供者而不是應用程序開發者來編寫.在Qt世界中,這種情況已發生了很大的變化.在用Qt編程其實就是在寫新的組件.典型的Qt應用程序都存在某些自定義的組件,在整個應用程序中被複用.相同的組件常常作爲其他程序的一部分被開發出來.KDE,K桌面環境,甚至使用許多附加庫,來進一步擴展Qt,實現許多額外的類.
但是一個優秀,高效的C++ API究竟是怎樣子呢?它的好壞取決於許多因素,比如說,手頭上的任務和特定目標羣體.優秀的API具有很多特性,它們的一些是普遍所要期望的,另一些是針對特定問題域的.
優秀API的六個特性
API對於程序員就相當於GUI對於最終用戶.API中'P'代表程序員(Programmer),而不是程序(Program),強調這一點是爲了說明API是讓程序員使用的,程序員是人而不機器.
我們認爲APIs應當精簡而完備,具有清晰簡單的語義,直觀,易記且應使代碼具有可讀性.
- 精簡性:精簡的API具有儘可能少的類和公共成員.這使得理解,記憶,調試,更改API更加容易.
- 完備性:完備的API意味着擁有應具有的期望功能.這可能使與API保持精簡性相沖突.還有,如果成員函數放在不相匹配的類中,那麼許多使用這個功能函數的潛在用戶會找不到它.
- 清晰簡單的語義:正如與其他設計工作一樣,你應該準守最小驚議原則.讓通常的任務簡單,罕見的任務應儘可能簡單,但它不應成爲重點.解決特定的問題.不要使解決方法具有普適作用,當它們不需要的時候.
- 直觀性:與計算機有關的其他事情一樣,API應具有直觀性.不同經歷和背景會導致對哪些是直觀,哪些不是直觀的不同看法.如果對非專業的用戶在不需要閱讀文檔下能立即使用API,或對這個API不瞭解的程序員能理解使用了API的代碼,那麼這API就是具有直觀性.
- 易記:爲了使API容易記憶,使用一致且精準的命名規範.使用容易識別的模式和概念,避免使用縮寫.
- 能生成可讀生代碼:代碼只寫一遍,卻要閱讀許多遍(調試或更改).可讀性的代碼有時候可能需要多敲些字,但是從產品生命週期中可節省很多時間.
最後,請記住:不同的用戶使用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);
初學者可能會按字面義理解爲,"不要重繪!"
自然的想法是bool參數節省了一個函數,因此減少了代碼的臃腫.事實上,這增加了代碼的臃腫,有多少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);
顯然的解決辦法就是將bool 參數用枚舉類型來替換.這就是我們在Qt 4中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自身都不能保持統一,事情自然會變得更壞.比如,Qt 3中有activatePreviousWindow()函數,也有fetchPrev()函數.堅持"沒有縮寫"這條規則,會使創建一致的API更加簡單.
在設計類中,另一重要但是不明顯的規則是儘量保持子類中名字的簡潔易懂.在Qt 3中,這個原則並不總是被遵守.爲了說明這一點,我們舉下QToolButton的例子.如果你在Qt 3中對QToolButton調用call name(), caption(), text(), 或 textLabel()成員函數時,你希望會發生什麼?那就在Qt設計器中試試QToolButton吧.
- name 屬性繼承自QObject,用來在調試和測試中指代對象的內部名稱.
- caption 屬性繼承自QWidget,指代窗體的標題.對於QToolButton沒有什麼意思,既然它們都是由父窗體創建的.
- text 屬性繼承自QButton,通常用於按鈕中,除非useTextLabel爲真.
- textLabel 屬性 在QToolButton中聲明,如果useTextLabel爲真,則顯示在按鈕上.
爲了可讀性的關係,在Qt4中name 被稱爲objectName ,caption被稱爲windowTitle,在QToolButton中爲了使text明晰,不再有textLabel屬性.
命名類
不應爲每個不同的類尋求完美的名字,而是將類進行分給.比如,在Qt 4中所有跟模型有關的視類的部件都用View後綴(QlistView,QTableView,QTreeView),相應的基於部件的類用Widget後綴代替(QListWidget,QTableWidget,QTreeWidge).
枚舉類型和值類型命名
當設計枚舉時,我們應當記住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,這樣做是類型不安全的.Qt 4提供了一模板類QFlags<T>,其中T是枚舉類型.Qt爲標誌類型名稱提供了便利,你能用Qt::Alignment 來代替QFlags<Qt::AlignmentFlag>.
爲了方便,我們給枚舉類型單數形式的名稱(只有當只含一個標誌位時),給"flags"類型複數形式的名稱,比如:
enum RectangleEdge { LeftEdge, RightEdge, ... }; typedef QFlags<RectangleEdge> RectangleEdges;
在某些情況下,"flags"類型有單數形式的名稱.在這種情況下,枚舉類型以Flag後綴標識:
enum AlignmentFlag { AlignLeft, AlignTop, ... }; typedef QFlags<AlignmentFlag> Alignment;
函數和參數的命名
函數命名中的一條規則就是應能從它的名字清楚地看出函數是否着副作用.在Qt 3中,常函數QString::simplifyWhiteSpace()就違反了這規則.即然它返回QString,而不是像它的名字所表述的那樣修改字符串. 在Qt 4中,這個函數被重命名爲QString::simplified().
參數名對於程序員來說是重要的信息來源,即使它們不出現在調用API的代碼中.既然現代的IDE會在程序員編碼時顯示這些參數,所以非常值得在頭文件中給這些參數取恰當的名字,在文檔中同樣使用相同的名字
給布爾型的getter,setter,屬性的命名
給布爾型的getter,setter,屬性取個恰當的名字總是特別困難.getter應該叫checked() 或者還是叫isChecked(),取scrollBarsEnabled()還是areScrollBarEnabled()
在Qt 4中,我們對於getter的函數使用下面的指導原則
- 形容詞就使用is-前綴.比如:
- isChecked()
- isDown()
- isEmpty()
- isMovingEnabled()
- scrollBarsEnabled(), not areScrollBarsEnabled()
- 動詞沒有前綴,也不使用第三人稱的(-s):
- acceptDrops(), not acceptsDrops()
- allColumnsShowFocus()
- 名詞性的通常沒有前綴:
- autoCompletion(), not isAutoCompletion()
- boundaryChecking()
- isOpenGLAvailable(), not openGL()
- isDialog(), not dialog()
setter的命名可以從這推知,只要去掉is前綴,在名字前面加set前綴就可以了.比如setDown()和setScrollBarsEnabled().屬性的名字跟getter一樣,就是沒有is前綴
Pointers or References?
指針或引用?
對於向外傳參,是使用指針,還是引用更好呢?
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在函數被調用後,其值極有可能被修改.
Case Study: QProgressBar
案例分析: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進行改進的關鍵之處就是需要觀察到Qt 4中QProgressBar與QAbstractSpinBox,以及它的子類,QSpinBox,QSlider,和QDial有着相似性.解決的辦法呢?將其中的progress和totalSteps用minimun,maximum和value替換.
增加valueChanged()的信號量.增加setRange()這一方便的函數.
接下來需要到progressString, percentage 和indicator實際上都指代同一東西:顯示在進度欄上的文本.通常這一文本是一百分數,但是它能被setIndicator()設置成任何值.這裏是新的API:
virtual QString text() const; void setTextVisible(bool visible); bool isTextVisible() const;
默認,這文本是百分比指示器.這可以用重新實現的text()進行改變.
在Qt 3中,setCenterIndicator() 和 setIndicatorFollowsStyle()是兩個影響對齊方式的函數.它們現在都被一個高級的函數所取代,setAlignment().
void setAlignment(Qt::Alignment alignment);
如果程序員沒有調用 setAlignment(),對齊是基於的樣式決定的.對於Motif樣式,文本顯示在中間,而對於其他樣式,文本是右對齊的.
這裏是改進過的QProgressBar:
class QProgressBar : public QWidget32 { ... 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); ... };
怎樣寫出正確的APIs
APIs需要質量保證.最早的版本一般都不是很好的,你必須測試它.通過調用這個API的代碼作爲測試事例,來驗證代碼具有可讀性.
另外的技巧包括讓人在沒有文檔和類文檔化(類的概述和函數說明)的情況下能夠使用這個API.
當你陷入麻煩中時,文檔化也是好的辦法找出一個合適的命名:試着爲這些類,函數,枚舉值標住文檔,然後使用浮現在你腦中的第一個詞彙.如果你找不到精準的名字去表述,那很有可能這個東西就不應存在.如果任何辦法都失敗了,而且你確信這個概念是有用的,那就發明一個新的名字吧.最後,不管怎麼說,"widget", "event", "focus", and "buddy"這些詞總會能用上一個.