"Designing Qt-Style C++ APIs" by Matthias Ettrich
http://doc.trolltech.com/qq/qq13-apis.html
翻譯這篇文章的目的不是讓人了解Qt,而是讓人試著學(xué)習(xí)點C++編程的軟技能。我從原文中得到的一些風(fēng)格上的體會,也希望你能從中有所收獲.(譯者注)
我們在Trolltech做了大量研究來改進(jìn)Qt開發(fā)體驗.在這篇文章中,我將分享我們的一些成果,呈現(xiàn)我們在進(jìn)行Qt 4設(shè)計時所使遵循的原現(xiàn),并向你展示如何將它們應(yīng)用到你的代碼中.
設(shè)計應(yīng)用程序接口(APIs)是有難度的.它是像跟設(shè)計編程語言一樣困難的藝術(shù).要遵循許多不同的的原則,這些原則中的許多還彼此沖突.
現(xiàn)今的計算機教育過多關(guān)注于算法和數(shù)據(jù)結(jié)構(gòu),很少去關(guān)注隱藏在程序設(shè)計語言和程序框架后面的那些設(shè)計原則.這使得程序員們面對日益重要的任務(wù),創(chuàng)建可復(fù)用的組件,毫無準(zhǔn)備.
在面向?qū)ο笳Z言出現(xiàn)前,通用的可復(fù)用的代碼大都由庫提供者而不是應(yīng)用程序開發(fā)者來編寫.在Qt世界中,這種情況已發(fā)生了很大的變化.在用Qt編程其實就是在寫新的組件.典型的Qt應(yīng)用程序都存在某些自定義的組件,在整個應(yīng)用程序中被復(fù)用.相同的組件常常作為其他程序的一部分被開發(fā)出來.KDE,K桌面環(huán)境,甚至使用許多附加庫,來進(jìn)一步擴(kuò)展Qt,實現(xiàn)許多額外的類.
但是一個優(yōu)秀,高效的C++ API究竟是怎樣子呢?它的好壞取決于許多因素,比如說,手頭上的任務(wù)和特定目標(biāo)群體.優(yōu)秀的API具有很多特性,它們的一些是普遍所要期望的,另一些是針對特定問題域的.
優(yōu)秀API的六個特性
API對于程序員就相當(dāng)于GUI對于最終用戶.API中'P'代表程序員(Programmer),而不是程序(Program),強調(diào)這一點是為了說明API是讓程序員使用的,程序員是人而不機器.
我們認(rèn)為APIs應(yīng)當(dāng)精簡而完備,具有清晰簡單的語義,直觀,易記且應(yīng)使代碼具有可讀性.
-
精簡性:精簡的API具有盡可能少的類和公共成員.這使得理解,記憶,調(diào)試,更改API更加容易.
-
完備性:完備的API意味著擁有應(yīng)具有的期望功能.這可能使與API保持精簡性相沖突.還有,如果成員函數(shù)放在不相匹配的類中,那么許多使用這個功能函數(shù)的潛在用戶會找不到它.
-
清晰簡單的語義:正如與其他設(shè)計工作一樣,你應(yīng)該準(zhǔn)守最小驚議原則.讓通常的任務(wù)簡單,罕見的任務(wù)應(yīng)盡可能簡單,但它不應(yīng)成為重點.解決特定的問題.不要使解決方法具有普適作用,當(dāng)它們不需要的時候.
-
直觀性:與計算機有關(guān)的其他事情一樣,API應(yīng)具有直觀性.不同經(jīng)歷和背景會導(dǎo)致對哪些是直觀,哪些不是直觀的不同看法.如果對非專業(yè)的用戶在不需要閱讀文檔下能立即使用API,或?qū)@個API不了解的程序員能理解使用了API的代碼,那么這API就是具有直觀性.
-
易記:為了使API容易記憶,使用一致且精準(zhǔn)的命名規(guī)范.使用容易識別的模式和概念,避免使用縮寫.
-
能生成可讀生代碼:代碼只寫一遍,卻要閱讀許多遍(調(diào)試或更改).可讀性的代碼有時候可能需要多敲些字,但是從產(chǎn)品生命周期中可節(jié)省很多時間.
最后,請記住:不同的用戶使用API的不同部分.當(dāng)簡單地使用Qt類的實例可能有直觀性,但這有可能使用戶在閱讀完有關(guān)文檔后,才能嘗試使用其中部分功能.
方便性陷阱
通常的誤讀是越少的代碼越能使你達(dá)到編寫更好的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");
布爾參數(shù)陷阱
布爾參數(shù)常常導(dǎo)致難以閱讀的代碼.特別地,增加某個bool參數(shù)到現(xiàn)存的函數(shù)一般都會是個錯誤的決定.在Qt中,傳統(tǒng)的例子是repaint(),它帶有一個可選的布爾參數(shù),來指定背景是否刪除(默認(rèn)是刪除).這就導(dǎo)致了代碼會像這樣子:
widget->repaint(false);
初學(xué)者可能會按字面義理解為,"不要重繪!"
自然的想法是bool參數(shù)節(jié)省了一個函數(shù),因此減少了代碼的臃腫.事實上,這增加了代碼的臃腫,有多少Q(mào)t用戶真正知道下面這三行代碼在做什么呢?
widget->repaint();
widget->repaint(true);
widget->repaint(false);
好一點的API代碼可能看起來像這樣:
widget->repaint();
widget->repaintWithoutErasing();
在Qt 4中,我們解決這個問題的辦法是,簡單地去除掉不刪除widget而進(jìn)行重繪的可能性.Qt 4對雙重緩沖的原生支持,會使這功能被廢棄掉.
這里有些例子:
widget->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Expanding, true);
textEdit->insert("Where's Waldo?", true, true, false);
QRegExp rx("moc_*.c??", false, true);
顯然的解決辦法就是將bool 參數(shù)用枚舉類型來替換.這就是我們在Qt 4中Qstring中的大小寫敏感所做的,比較下面兩個例子:
str.replace("%USER%", user, false); // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4
靜態(tài)多態(tài)
相似的類應(yīng)該有相似的API.在某種程度上,這能用繼承來實現(xiàn),也就是運用運行時多態(tài)機制.但是多態(tài)也能發(fā)生在設(shè)計時.比如,你將QListBox與QComboBox交換,QSlider與QSpinBox交換,你會發(fā)現(xiàn)API的相似性會使這種替換變得比較容易.這就是我們所謂的"靜態(tài)多態(tài)".
靜態(tài)多態(tài)也能使記憶API和編程模式更加容易.因而,對一組相關(guān)類的相似API有時候比為每個類設(shè)計獨特完美的API會更好.
命名藝術(shù)
命名有時候是設(shè)計API中最重要的事情了.某個類應(yīng)叫什么名字,某個成員函數(shù)又應(yīng)叫什么名字,都需要好好思考.?
通常的命名規(guī)則
有少許規(guī)則對所有類型的命名都適應(yīng).首先,正如我早先所提到的,不要用縮寫.甚至對用"prev"代表"previous"這樣明顯的縮寫也不會在長期中受益,因為用戶必須記住哪些名字是縮寫.
如果連API自身都不能保持統(tǒng)一,事情自然會變得更壞.比如,Qt 3中有activatePreviousWindow()函數(shù),也有fetchPrev()函數(shù).堅持"沒有縮寫"這條規(guī)則,會使創(chuàng)建一致的API更加簡單.
在設(shè)計類中,另一重要但是不明顯的規(guī)則是盡量保持子類中名字的簡潔易懂.在Qt 3中,這個原則并不總是被遵守.為了說明這一點,我們舉下QToolButton的例子.如果你在Qt 3中對QToolButton調(diào)用call name(), caption(), text(), 或 textLabel()成員函數(shù)時,你希望會發(fā)生什么?那就在Qt設(shè)計器中試試QToolButton吧.
-
name 屬性繼承自QObject,用來在調(diào)試和測試中指代對象的內(nèi)部名稱.
-
caption 屬性繼承自QWidget,指代窗體的標(biāo)題.對于QToolButton沒有什么意思,既然它們都是由父窗體創(chuàng)建的.
-
text 屬性繼承自QButton,通常用于按鈕中,除非useTextLabel為真.
-
textLabel 屬性 在QToolButton中聲明,如果useTextLabel為真,則顯示在按鈕上.
為了可讀性的關(guān)系,在Qt4中name 被稱為objectName ,caption被稱為windowTitle,在QToolButton中為了使text明晰,不再有textLabel屬性.
命名類
不應(yīng)為每個不同的類尋求完美的名字,而是將類進(jìn)行分給.比如,在Qt 4中所有跟模型有關(guān)的視類的部件都用View后綴(QlistView,QTableView,QTreeView),相應(yīng)的基于部件的類用Widget后綴代替(QListWidget,QTableWidget,QTreeWidge).
枚舉類型和值類型命名
當(dāng)設(shè)計枚舉時,我們應(yīng)當(dāng)記住C++中(不像Java或C#),枚舉值在使用時不帶類型名.下面的例子說明了對枚舉值取太一般化的名字的危害:
namespace Qt
{
enum Corner { TopLeft, BottomRight, ... };
enum CaseSensitivity { Insensitive, Sensitive };
...
};
tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);
在上面這行中,Insensitive這個名字什么意思呢?為枚舉類型命名具有指導(dǎo)的原則是最好在每個枚舉值中重復(fù)枚舉類型的名字.
namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, ... };
enum CaseSensitivity { CaseInsensitive,
CaseSensitive };
...
};
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
但枚舉值之間是一種"或"關(guān)系和被用作標(biāo)志位時,傳統(tǒng)的解決方法是將"或"結(jié)果存為int,這樣做是類型不安全的.Qt 4提供了一模板類QFlags<T>,其中T是枚舉類型.Qt為標(biāo)志類型名稱提供了便利,你能用Qt::Alignment 來代替QFlags<Qt::AlignmentFlag>.
為了方便,我們給枚舉類型單數(shù)形式的名稱(只有當(dāng)只含一個標(biāo)志位時),給"flags"類型復(fù)數(shù)形式的名稱,比如:
enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef QFlags<RectangleEdge> RectangleEdges;
在某些情況下,"flags"類型有單數(shù)形式的名稱.在這種情況下,枚舉類型以Flag后綴標(biāo)識:
enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef QFlags<AlignmentFlag> Alignment;
函數(shù)和參數(shù)的命名
函數(shù)命名中的一條規(guī)則就是應(yīng)能從它的名字清楚地看出函數(shù)是否著副作用.在Qt 3中,常函數(shù)QString::simplifyWhiteSpace()就違反了這規(guī)則.即然它返回QString,而不是像它的名字所表述的那樣修改字符串. 在Qt 4中,這個函數(shù)被重命名為QString::simplified().
參數(shù)名對于程序員來說是重要的信息來源,即使它們不出現(xiàn)在調(diào)用API的代碼中.既然現(xiàn)代的IDE會在程序員編碼時顯示這些參數(shù),所以非常值得在頭文件中給這些參數(shù)取恰當(dāng)?shù)拿郑谖臋n中同樣使用相同的名字
給布爾型的getter,setter,屬性的命名
給布爾型的getter,setter,屬性取個恰當(dāng)?shù)拿挚偸翘貏e困難.getter應(yīng)該叫checked() 或者還是叫isChecked(),取scrollBarsEnabled()還是areScrollBarEnabled()
在Qt 4中,我們對于getter的函數(shù)使用下面的指導(dǎo)原則
- 形容詞就使用is-前綴.比如:
- isChecked()
- isDown()
- isEmpty()
- isMovingEnabled()
但是形容詞應(yīng)用到復(fù)數(shù)形式的名詞沒有前綴:
- scrollBarsEnabled(), not areScrollBarsEnabled()
- 動詞沒有前綴,也不使用第三人稱的(-s):
- acceptDrops(), not acceptsDrops()
- allColumnsShowFocus()
- 名詞性的通常沒有前綴:
- 用autoCompletion(), 不用isAutoCompletion()
- boundaryChecking()
有時候沒有前綴會產(chǎn)生誤導(dǎo),在這種情就加上前綴is-:
- isOpenGLAvailable(), not openGL()
- isDialog(), not dialog()
(如果函數(shù)叫做dialog(),我們通常會認(rèn)定它會返回QDialog*類型)
setter的命名可以從這推知,只要去掉is前綴,在名字前面加set前綴就可以了.比如setDown()和setScrollBarsEnabled().屬性的名字跟getter一樣,就是沒有is前綴
指針或引用?
對于向外傳參,是使用指針,還是引用更好呢?
void getHsv(int *h, int *s, int *v) const
void getHsv(int &h, int &s, int &v) const
絕大多數(shù)C++書籍都推薦無論何時都盡可能使用引用,因為從大多數(shù)情況來說,引用比指針有著所謂的"安全和優(yōu)雅".相比而方,在Trolltech,我們更趨向于指針,因為它使用戶代碼更具可讀性.比較下面的代碼:
color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);
只有第一行代碼能更清楚地說明h,s,v在函數(shù)被調(diào)用后,其值極有可能被修改.
案例分析:QProgressBar
為了在實際代碼中說明這些概念,我們以QProgressBar在Qt3和Qt4中的比較進(jìn)行研究.在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進(jìn)行改進(jìn)的關(guān)鍵之處就是需要觀察到Qt 4中QProgressBar與QAbstractSpinBox,以及它的子類,QSpinBox,QSlider,和QDial有著相似性.解決的辦法呢?將其中的progress和totalSteps用minimun,maximum和value替換.
增加valueChanged()的信號量.增加setRange()這一方便的函數(shù).
接下來需要到progressString, percentage 和indicator實際上都指代同一東西:顯示在進(jìn)度欄上的文本.通常這一文本是一百分?jǐn)?shù),但是它能被setIndicator()設(shè)置成任何值.這里是新的API:
virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
默認(rèn),這文本是百分比指示器.這可以用重新實現(xiàn)的text()進(jìn)行改變.
在Qt 3中,setCenterIndicator() 和 setIndicatorFollowsStyle()是兩個影響對齊方式的函數(shù).它們現(xiàn)在都被一個高級的函數(shù)所取代,setAlignment().
void setAlignment(Qt::Alignment alignment);
如果程序員沒有調(diào)用 setAlignment(),對齊是基于的樣式?jīng)Q定的.對于Motif樣式,文本顯示在中間,而對于其他樣式,文本是右對齊的.
這里是改進(jìn)過的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需要質(zhì)量保證.最早的版本一般都不是很好的,你必須測試它.通過調(diào)用這個API的代碼作為測試事例,來驗證代碼具有可讀性.
另外的技巧包括讓人在沒有文檔和類文檔化(類的概述和函數(shù)說明)的情況下能夠使用這個API.
當(dāng)你陷入麻煩中時,文檔化也是好的辦法找出一個合適的命名:試著為這些類,函數(shù),枚舉值標(biāo)住文檔,然后使用浮現(xiàn)在你腦中的第一個詞匯.如果你找不到精準(zhǔn)的名字去表述,那很有可能這個東西就不應(yīng)存在.如果任何辦法都失敗了,而且你確信這個概念是有用的,那就發(fā)明一個新的名字吧.最后,不管怎么說,"widget", "event", "focus", and "buddy"這些詞總會能用上一個.
posted on 2008-05-11 20:07
len 閱讀(7801)
評論(6) 編輯 收藏 引用 所屬分類:
程序開發(fā)