原文地址:API Design Principles http://qt-project.org/wiki/API-Design-Principles
摘要:
此文爲Qt 官網上的API設計(for C++)指導準則,其中有不少原則具有普遍適用性,整個篇幅中有很多示例,是Qt在API設計上的實踐。
正文:
Qt 一致、易掌握、強大的API是它的衆多著名的優點之一。此文總結了我們在設計Qt風格的API所積累的做法。其中許多準則是通用的;而其他內容更偏向與約定,遵守它主要是爲了與已有的API保持一致。
雖然這些準則主要用於公共API, 你也可以在設計私有API時使用它們,把它作爲與其他開發者之間的約定。
1.好API的6個特點(Six Characteristics of Good API)
API對與程序員的關係就如同GUI與用戶的關係。API中的’P’實際上指的是’程序員’,而不是’程序’,它強調了所有API都是由程序員使用的事實。
Matthias 在他的Qt Quarterly 13 article about API design 中說他相信API應該很少並且是完整的,有清晰簡單的語義,可憑直覺知道的,容易記憶而且能產生可讀的代碼。
數量最小化(Be minimal)
數量最小化的API擁有儘可能少的公有類,每個公有類的公有成員也很少。依次可使理解,記憶,調試,改變API更容易。
功能完整(Be complete)
完整的API包含了所有需要用到的功能。這和API的數量最小化有點衝突。另外,如果某個成員函數放到了錯誤的類裏,需要使用此方法的用戶就會找不到它。
清晰簡單的語義(Have clear and simple semantics)
如同其他的設計,我們應該遵守意外最少的原則(principle of least surprise),把常用的操作變得容易,很少用的操作不應該很顯眼。不要把沒必要通用的解決方案普遍化。例如,在Qt 3 中,QMimeSourceFactory 不應被命名爲 QImageLoader。
直覺性強(Be intuitive)
API 應該有較強的直覺性,就像計算機裏的其他內容,。不同的履歷和背景導致了不同人有不同直覺。如果一個經驗不很豐富的用戶沒有閱讀文檔就能搞懂某個API,明白此API構成的代碼,說明此API直覺性較強。
容易記憶( Be easy to memorize)
爲使API易於記憶,應選擇一個具有一致性和精確性的命名約定。使用易於識別的模式和概念,避免使用縮寫。
能寫出可讀代碼(Lead to readable code)
代碼只寫一次,但是卻被瀏覽,調試,改變很多次。可讀性好的代碼可能需要更長的時間來寫,卻能在產品的整個生命週期中節省時間。
最後,記住不同的用戶使用不同部分的API。儘管記住簡單得使用一個Qt類的示例更直接,最好還是告訴用戶在使用API時閱讀相關一下文檔。
2.靜態多態(Static Polymorphism)
相似的類應該有相似的API。運行時多態通過繼承體系實現。然而多態也發生在設計階段。例如,如果你用QProgressBar替換QSlider,或者是QString替換QByteArray,你會發現API的簡潔性使替換如此容易。此所謂“靜態多態”。
靜態多態也使記憶API和編程模式更加容易。因此,一組有相似接口的相關類比每個類都有自己的一套接口更好。
總體上講,在Qt中,比起沒有強有力的理由實現的繼承關係,我們跟喜歡依賴靜態多態。這種做法可確保公有類較少,使剛學習Qt的用戶認清路線。
好的靜態多態
QDialogButtonBox與 QMessageBox 在按鈕操作(addButton(), setStandardButtons()等等 )有相似的API,而沒有繼承某個”QAbstractButtonBox”的類。
糟糕的靜態多態
QTcpSocket 與 QUdpSocket 都繼承了 QAbstractSocket ,它們是兩個行爲非常不同的類。似乎沒有什麼人曾經或會去使用一個QAbstractSocket指針。
不能確定的靜態多態
QBoxLayout 是 QHBoxLayout 與 QVBoxLayout 的基類,好處:可以在工具欄中使用QBoxLayout調用setOrientation() 使其變爲水平/垂直。壞處:需要一個額外的類,並且有可能導致用戶寫出這樣沒什麼意義的代碼,((QBoxLayout *)hbox)->setOrientation(Qt::Vertical)。
3. 基於特性的API(Property-Based APIs )
新的Qt類傾向於基於特性的API,例如:
QTimer timer;
timer.setInterval(1000);
timer.setSingleShot(true);
timer.start();
特性,是指此對象的任何屬性——無論它是否是Q_PROPERTY。在實踐中,用戶應能夠以任何順序設置這些特性,也就是說,這些特性應該是正交的。例如,之前的代碼可以寫成:
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(1000);
timer.start();
【譯者注:正交特性:改變某個特性而不會影響到其他的特性。《程序員修煉之道》中講了一個關於正交性的直升飛機墜毀的例子。】
方便起見,也寫成:
timer.start(1000);
類似地,對於QRegExp,
QRegExp regExp;
regExp.setCaseSensitive(Qt::CaseInsensitive);
regExp.setPattern("***.*");
regExp.setPatternSyntax(Qt::WildcardSyntax);
爲實現這種類型的API,最好不要過早構建目標。例如,在QRegExp的例子中,在不知道模式語法的時候,在setPattern()中編譯"***.*"爲時過早。
各種特性經常關聯在一起。在上面的例子中,我們必須小心。思考一下當前風格提供的“默認的圖標尺寸”和QToolButton的”iconSize”屬性:
toolButton->iconSize(); // returns the default for the current style
toolButton->setStyle(otherStyle);
toolButton->iconSize(); // returns the default for otherStyle
toolButton->setIconSize(QSize(52, 52));
toolButton->iconSize(); // returns (52, 52)
toolButton->setStyle(yetAnotherStyle);
toolButton->iconSize(); // returns (52, 52)
需要提醒的是,一旦設置了 iconSize,它就一直保持設置狀態;改變當前的風格不會改變此事。這樣很好。有時候,重置某個特性也很有用,有兩種方法:
(1)傳入一個特殊值 (如 QSize(), -1, 或者 Qt::Alignment(0)) ,意味着重置
(2)提供一個明確的重置接口,如 resetFoo() 和 unsetFoo() 對於iconSize,使用Qsize()就足夠了。
在某些情況下,getters 返回的結果與設置的可能不同。例如,如果調用widget->setEnabled(true),如果它的parent處於disabled狀態,widget->isEnabled()仍然返回 false。這樣是可行的,正是我們想要的(widget的父widget處於disabled狀態,此widget也應該變爲灰色,就好象也處於disabled狀態一樣,但是它會記得,其本身並沒有處於disabled狀態,正等待它的父widget變爲enabled.),但是諸如這樣的特性必須被詳細列入文檔。
4.C++ API 規範(C++ Specifics)
指針與引用(Pointers vs. References)
最好的輸出參數的類型是什麼,指針還是引用?
void getHsv(int *h, int *s, int *v) const
void getHsv(int &h, int &s, int &v) const
大多數C++書籍基於引用比指針“安全和優雅”的觀點,推薦儘可能使用引用。相比之下,我們在開發Qt時更喜歡指針,因爲使用指針可使用戶代碼可讀性更好。比較下面兩個例子:
color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);
僅有第一行代碼充分顯示h,s,v 很可能被此函數調用修改。虛函數(Virtual Functions)
類的成員函數聲明爲virtual,一般是爲了通過其子類實現此函數來定製函數的行爲。將函數聲明爲virtual的目的是爲了實現動態多態。如果在類外面沒有人調用聲明爲virtual的函數,將其聲明爲virtual之前,你應該多加小心。
// QTextEdit in Qt 3: member functions that have no reason for being virtual
virtual void resetFormat();
virtual void setUndoDepth( int d );
virtual void setFormat( QTextFormat *f, int flags );
virtual void ensureCursorVisible();
virtual void placeCursor( const QPoint &pos;, QTextCursor **c = 0 );
virtual void moveCursor( CursorAction action, bool select );
virtual void doKeyboardAction( KeyboardAction action );
virtual void removeSelectedText( int selNum = 0 );
virtual void removeSelection( int selNum = 0 );
virtual void setCurrentFont( const QFont &f );
virtual void setOverwriteMode( bool b ) { overWrite = b; }
QTextEdit從Qt3 移植到Qt4 的時候,幾乎所有的虛函數都被移除了。有趣的是(不過並不是沒有預料到的),沒有人對此抱怨,爲什麼?因爲Qt3根本沒用到QTextEdit的多態行爲。簡單得說,沒有理由去繼承QTextEdit並重新實現這些函數,除非你自己調用了這些方法。如果你需要在你的應用程序,其中不屬於Qt的部分實現多態,你會自己增加。
在Qt中,我們有很多理由儘量減少虛函數的數量。每一次對虛函數的調用會在函數調用圖中插入一個未掌控的節點(某種程度上使結果無法預測),使修改bug變得複雜。用戶在重新實現的虛函數中能做很多事:
—— 發送事件
——發送信號
——從新進入事件循環(例如,通過打開一個模式文件對話框)
——刪除對象(導致了delete this)
還有一些其他原因說明應避免過分使用虛函數:
(1)重載虛函數有難度。
(2)編譯器很難優化虛函數或者讓其變爲inline函數。
(3)調用虛函數需要查找虛函數表,這比調用一個普通函數慢了2到3倍。
(4)虛函數使類很難按值複製(可以做到,但是非常混亂,不建議這樣做)
經驗告訴我們,沒有虛函數的類一般有更少的bug,需要更少的維護成本。
一個重要的準則是除非我們作爲工具集提供而且很多用戶會調用某類的某個虛函數,否則不會這個函數設計成虛函數。
虛函數與複製(Virtualness vs. copyability)
包含虛函數的類必須把析構函數聲明爲虛函數,以防止基類析構時沒有執行子類析構函數,導致內存泄漏。
如果要使某個類可以複製可以賦值,或者能夠按值比較,應該需要一個拷貝構造函數,一個operator=() 和一個 operator==()。
class CopyClass {
public:
CopyClass();
CopyClass(const CopyClass &other);
~CopyClass();
CopyClass &operator=(const CopyClass &other);
bool operator== (const CopyClass &other) const;
bool operator!=(const CopyClass &other) const;
virtual void setValue(int v);
};
如果有的類從CopyClass繼承,未預料到的事就可能發生在代碼中。一般情況下,如果沒有虛成員函數和虛析構函數,就不能有依賴多態的子類。然而,如果存在虛成員函數和虛析構函數,這突然變成了要有子類去繼承某個類的理由,而且也變的複雜了。最初容易獲知只要簡單得聲明虛的操作符重載函數。但是如此下去會導致一篇混亂和不可讀的代碼。見如下例子:
class OtherClass {
public:
const CopyClass &instance() const; // what does it return? What should I assign it to?
};
常數(Constness)
C++ 的 關鍵詞“const”表明了某些內容不可變或者沒有副作用,它適用與簡單的值,指針,指針所指的內容,或者類的成員函數。
然而,const並沒有提供太大的價值——很多編程語言甚至沒有類似”const”的關鍵詞,但是卻並沒有因此產生問題。實際上,如果你不用函數重載,並刪除你的C++代碼中所有的”const”,它也幾乎能編譯通過並且正常運行。
讓我們看一下與Qt的API設計相關的應用”const”的部分。
輸入參數:const 類型的指針(Input arguments:const pointers)
參數是指針類型類型的const成員函數,它們的指針參數幾乎總是const類型的。
若將函數聲明爲 const類型的(例如:bool func() const;),意味着它既沒有副作用,也不會改變對象的狀態。那爲什麼它需要一個沒有const限定的輸入參數呢?記住const類型的函數通常被其他const類型的函數調用,所以不能傳入非const的指針(沒有用const_cast,應該儘量避免使用const_cast)。
以前:
bool QWidget::isVisibleTo(QWidget *ancestor) const;
bool QWidget::isEnabledTo(QWidget *ancestor) const;
QPoint QWidget::mapFrom(QWidget *ancestor, const QPoint &pos) const;
QWidget 聲明瞭許多參數本身可變的const類型的成員函數,這些函數可以修改傳入的參數,不能修改對象自身。這樣的函數通常也包含了const_cast轉換。這些函數能最好接受的是const的參數。
之後:
bool QWidget::isVisibleTo(const QWidget *ancestor) const;
bool QWidget::isEnabledTo(const QWidget *ancestor) const;
QPoint QWidget::mapFrom(const QWidget *ancestor, const QPoint &pos) const;
請注意,我們在QGraphicsItem中對此做了修正,但是QWidget 要等到 Qt 5:
bool isVisibleTo(const QGraphicsItem *parent) const;
QPointF mapFromItem (const QGraphicsItem *item, const QPointF &point) const;
返回值:const類型的值(Return values:const values)
調用函數獲得的非引用類型的值,稱之爲右值(R-value)。
不是類的對象的右值一般沒有const限定(cv-unqualified type)。 雖然從語法上講,用const限定這種類型也可以,但是卻沒有意義,因爲這不改變任何與訪問權限有關的內容。
【譯者注:cv-qualified的類型(與cv-unqualified 相反) 是由const 或者volatile 或者volatile const限定的類型。】
大部分編譯器在編譯這樣的代碼時會提示警告信息。
若用”const”限定作爲右值的類的對象,訪問這個對象非const類型的成員函數或直接修改它的成員都是不可能的。
不加const可以去除以上限制,但隨着作爲右值的對象生命週期在分號出現時結束,這樣做幾乎沒有必要。
示例:
struct Foo
{
void setValue(int v) { value = v; }
int value;
};
Foo foo()
{
return Foo();
}
const Foo cfoo()
{
return Foo();
}
int main()
{
// The following does compile, foo() is non-const R-value which
// can't be assigned to (this generally requires an L-value)
// but member access leads to a L-value:
foo().value = 1;
// Ok, but temporary will be thrown away at the end of the full-expression.
// The following does compile, foo() is non-const R-value which
// can't be assigned to, but calling (even non-const) member
// function is fine:
foo().setValue(1);
// Ok, but temporary will be thrown away at the end of the full-expression.
// The following does _not_compile, foo() is _const_ R-value
// with const member which member access can't be assigned to:
cfoo().value = 1; // Not ok.
// The following does _not_compile, foo() is _const_ R-value,
// one cannot call non-const member functions:
cfoo().setValue(1); // Not ok
}
返回值:沒有const的指針還是有const的指針(Return values:pointers vs. const pointers)
談到const類型的函數應該返回非const的指針還是const指針的話題時,多數人發現“const 正確性”(const correctness)在C++中產生了分歧。問題源於const類型的成員函數,其本身不修改對象的狀態,返回了對象的非const的成員的指針。僅僅是返回這樣的指針不會影響對象的狀態,也不改變函數的行爲,但卻給其他程序員修改對象狀態的機會。下面的例子展示了調用返回值是沒有const限定的指針的用const限定的函數,卻能規避const特性的一種方式。
QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const
{
moveBy(10, 10); // doesn't compile!
window()->childAt(mapTo(window(), rect().center()))->moveBy(10, 10); // compiles!
}
返回const指針的函數至少從一定程度上避免了這種副作用(也許是不期望的,沒有預料到的)的產生。
若採用const-correct的方法,每個返回某個數據成員的指針(或多個數據成員的指針)的const類型的函數必須返回const的指針。在實踐中,這種做法將導致沒有用的API:
QGraphicsScene scene;
// ... populate scene
foreach (const QGraphicsItem *item, scene.items()) {
item->setPos(qrand() % 500, qrand() % 500);
// doesn't compile! item is a const pointer
}
QGraphicsScene::items() 是一個const類型的成員函數。
在Qt中,我們根據特定情況使用非const。我們找到了一個實用的方法:當返回const指針後,招致過分使用const_cast帶來的問題多於濫用返回非const的指針時,我們會選擇返回f非const的指針。
返回值:返回值還是返回const的引用(Return values: by value or const reference?)
若返回的是複製的對象,那麼返回const引用可以更快;然而,以後對這個類的重構(refactor)將受限。我們可以任意改變Qt類在內存中的組織形式,但卻不能在不破壞程序兼容性的情況下把返回值從“const QFoo &”變爲”QFoo”。改變因此,除去個別速度非常重要而重構不是問題的情形(例如,QList::at())外,我們一般返回”QFoo”而不是”const QFoo &”。
Const 還是對象的狀態(Const vs. the state of an object )
C++中有關Const 正確性(const coreectness)的問題就像vi 和emacs,因爲這個問題在很多地方都存在分歧(比如包含指針的函數)。
但是通用的準則是const類型的成員函數不改變對象狀態。“狀態”的意思是“自身以及自身的行爲”。這並不是說非const的成員函數要改變私有成員,而是這樣的函數存在可見的副作用(visible side effects)。const類型的函數一般沒有什麼副作用,比如:
QSize size = widget->sizeHint(); // const
widget->move(10, 10); // not const
某個widget負責繪製其他的內容。它的狀態包括它的行爲,因此也包括了它畫出來的對象的狀態。調用它的繪畫行爲必然會有副作用;它改變了它繪製的設備的外觀(以及狀態)。由此,用const限定paint()完全沒有必要。QIcon的paint()也不需要是const的,沒有人會在const類型函數的內部調用QIcon::paint(),除非他想顯式的迴避const這個特性。如果是前述情況,應使用const_cast。
// QAbstractItemDelegate::paint is const
void QAbstractItemDelegate::paint(QPainter **painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
// QGraphicsItem::paint is not const
void QGraphicsItem::paint(QPainter * painter, const QStyleOptionGraphicsItem **option, QWidget *widget h1. 0)
如果const關鍵字不起作用,應該考慮將其移除而不是重載函數的non-const版本。
5.API語義和文檔(API Semantics and Documentation)
如果傳入了一個值爲-1的參數,函數的行爲是什麼?警告、致命錯誤還是....有很多類似的問題...
API需要的是質量保證。第一個版本的API不可能是沒問題的;必須對其進行測試。閱讀使用API的代碼,發現用例並且檢查代碼的可讀性。
其他的方法包括讓其他人在有文檔(有關類及其方法的文檔)或沒有文檔輔助的情況下使用你的API。
6.命名的藝術(The Art of Naming)
命名很可能是設計API時最簡單最重要的方面。各個類的名稱如何確定?成員函數名稱如何確定?
通用的命名規則(General Naming Rules)
有幾個規則對所有的命名都適用。第一個是,之前已經說過的,不要使用縮寫,即使是明顯的縮寫,比如把”previous”縮寫成”prev”從長遠來看並不值,因爲用戶必須記住縮寫詞的實際含義。
如果API本身一致性不好,事情就會越來越糟;例如,Qt3 中同時存在activatePreviousWindow()與fetchPrev()。恪守“不縮寫”規則使建立一致性的API更容易。
另一個在設計類時重要但是更微妙的規則是應該保持子類名稱空間的乾淨。在Qt3中,此項準則沒有被一直追隨。爲了對此進行說明,我們以QToolButton爲例。如果調用某個QToolButton的 name()、caption()、text()或者textLabel(),你期望獲得什麼? 在Qt Designer試着研究一下QToolButton:
(1)name函數是從QObject繼承而來,它返回的是內部的對象名稱,可以被用來debug或者測試。
(2)caption函數繼承自QWidget ,它返回的是窗口標題,對QToolButton來說毫無意義,因爲它在創建的時候parent就存在了。
(3)text函數繼承自QButton,一般被Button使用,useTextLabel爲true使不使用。
(4)textLabel是在QToolButton內部定義的,只有useTextLabel爲true時,其內容才顯示在Button上。
爲了具有可讀性,name在Qt4中叫做objectName,caption改爲windowTitle,QToolButton中再也沒有textLabel了。
當你找不到好的名稱時,開始寫文檔是一種好好的尋找方式:嘗試爲類、方法、枚舉類型、值等寫文檔,把寫下的第一句作爲啓發。如果找不確切的名稱,這說明這個東西不該存在。如果所有嘗試都失敗了,並且你認爲不如發明一個新名稱,你就知道”widget”,“event”,”focus”和”buddy”是如何產生的了。
類的命名(Naming Classes)
用把類的名稱分組的方式替換爲每個類單獨命名的方法。例如,所有Qt4的瞭解模型(model-aware)的視圖(view)類後綴都是View(QListView,QTableView,QTreeView),相應的基於item的類後綴是Widget(QListWidget,QTableWidget,QTreeWidget)。
枚舉類型和值的命名(Naming Enum Types and Values)
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);
當對枚舉值進行或運算並作爲某種標誌(flag)時,傳統的做法是把或運算的結果保存在int型的值中,這不是類型安全的。Qt4提供了一個模板類,QFlags<T>,其中的T是枚舉類型。爲方便使用,Qt用typedef重新定義了QFlag類型, 所以可以用Qt::Alignment 代替QFlags<Qt::AlignmentFlag>。
習慣上,枚舉類型命名爲單數名詞(因爲它一次只能“持有”一個flag),把可容納多個”flag”的類型用複數命名,例如:
enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef QFlags<RectangleEdge> RectangleEdges;
在某性情形下,這種可容納多個”flag”的類型名稱爲單數形式。而枚舉類型的後綴變爲 “Flag”:
enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef QFlags<AlignmentFlag> Alignment;
函數和參數的命名(Naming Functions and Parameters)
函數命名的第一準則是可以從名稱看出來此函數是否有副作用。在Qt3中,QString::simplifyWhiteSpace() 違反了此準則,因爲它返回了一個QString 而不是按名稱暗示的那樣,改變調用它的QString對象。在Qt4中,此函數重命名爲QString::simplified()。
雖然參數名稱不會在使用API的代碼中出現,但是它們給程序員提供了重要信息。因爲現在的IDE都會在寫代碼時顯示參數名稱,在頭文件中給參數起一個恰當的名稱並在文檔中使用相同的名稱很值得。
Bool類型的getter與setter的命名(Naming Boolean Getters, Setters, and Properties )
爲bool成員的獲取函數(getter)和設置函數(setter)命名真痛苦。Getter應該叫做checked()還是isChecked()? scrollBarsEnabled() 或者areScrollBarEnabled()?
Qt4中,我們套用以下準則爲getter命名:
(1)形容詞以is-爲前綴,例子:
isChecked(),
isDown() ,
isEmpty(),
isMovingEnabled()
(2)然而,修飾名詞的形容詞沒有前綴:
scrollBarsEnabled(), 而不是 areScrollBarsEnabled()
(3)動詞沒有前綴,也不使用第三人稱(-s):
acceptDrops(), not acceptsDrops()
allColumnsShowFocus()
(4)名詞一般沒有前綴:
autoCompletion(), 而不是isAutoCompletion()
boundaryChecking()
(5) 有時,沒有前綴容易混淆,我們會加上is-前綴:
isOpenGLAvailable(), 而不是 openGL()
isDialog(), 而不是 dialog()
(一個叫做dialog()的函數,一般會被認爲是返回 QDialog **。)
Setter的名稱來源於getter,只是去掉了is-前綴,在前面加上了set;例如,setDown() 與setScrollBarsEnabled()。
7.避免常見陷阱(Avoiding Common Traps)
簡化的陷阱(The Convenience Trap)
實現某樣東西需要寫的代碼越少,API設計的越好這種觀點是一種誤解。應該記住代碼只寫一次,卻被多次閱讀和理解。例如:
QSlider *slider h1. new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");
這段代碼比下面這個難理解多了: QSlider *slider h1. new QSlider(Qt::Vertical);
slider->setRange(12, 18);
slider->setPageStep(3);
slider->setValue(13);
slider->setObjectName("volume");
Boolean值的陷阱(The Boolean Parameter Trap )
Bool類型的參數總是帶來無法閱讀的代碼。給現有的函數增加一個bool型的參數幾乎永遠是一種錯誤的行爲。仍以Qt爲例,repaint()有一個bool類型的可選參數用於指定背景是否被擦出。可以寫出這樣的代碼:
widget->repaint(false);
初學者很可能是這樣理解的,”不要重新繪製!”(Don’t repaint!),能有多少Qt用戶真心知道下面3行是什麼意思:
widget->repaint();
widget->repaint(true);
widget->repaint(false);
更好的API設計應該是這樣的:
widget->repaint();
widget->repaintWithoutErasing();
在Qt4中,我們通過移除了重新繪製(repaint)而不擦出widget的能力來解決了此問題。Qt4的雙緩衝使這種特性被廢棄。
還有更多的例子:
widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding, true);
textEdit->insert("Where's Waldo?", true, true, false);
QRegExp rx("moc_***.c??", false, true);
一種較爲明顯的解決方案是使用枚舉值替代bool類型的值。我們在Qt4中的QString使用了此方法,對下面兩種方式作一個比較:
str.replace("%USER%", user, false); // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4
8.案例研究(Case Studies)
進度條(QProgressBar)
爲了展示上文各種準則的實際應用。我們來學習一下Qt3中QProgressBar的API,並與Qt4中對應的API作比較。
在Qt3中:
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中對此不明晰。
改善此API的關鍵是注意到QProgressBar與Qt4的QAbstractSpinBox及其子類QSpinBox,QSlider,QDail有相似之處。怎麼做?把progress,totalSteps替換爲minimum,maximum和value。增加一個valueChanged() 消息,再增加一個 setRange() 函數。
下一個發現是progressString, percentage 與 indicator其實是一回事:顯示在進度條上的文字。通常這個文字是某個百分數,但是可通過setIndicator()設置爲任何內容。以下是新的API:
virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
默認情況下,文字的內容是百分數,重寫text()可以對此作改變。
Qt3的setCenterIndicator() 與 setIndicatorFollowsStyle() 是兩個影響對齊的函數。他們可被一個setAlignment()函數代替:
void setAlignment(Qt::Alignment alignment);
如果API用戶未調用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);
...
};
QAbstractItemModel
有關模型/視圖(model/view)的問題的細節在其他地方已經作了描述,但是有一個重要的總結是某個抽象的類不應該僅僅是聯合(union)所有可能的子類的。這樣的抽象基類幾乎不是一個好的方案。QAbstractItemModel 就犯了這個錯誤——它其實是QTreeOfTablesModel,包含了一些複雜的API,但卻被很多類繼承。僅僅是增加抽象不會把API設計得更好。
QImageSink
Qt3有多個類用來逐漸加載圖像並做成動畫——QImageSource/Sink/QASyncIO/QASyncImageIO。由於他們完全可由變換的QLabel代替,所以這些類全都被移除了。 我們獲得的教訓是不要通過增加抽象來應對某些模糊的未來情況。保持簡潔,當這些情況出現時, 把它們納入一個簡單的系統要比納入一個複雜的系統容易得多。
Materials:
[1] Little Manual of API Design http://chaos.troll.no/~shausman/api-design/api-design.pdf
[2] Qt Quarterly 13 article about API design
http://doc.qt.nokia.com/qq/qq13-apis.html
[3] “The Pragmatic Programmer”-《程序員修煉之道》(強烈推薦此書,絕對是必讀之書)
轉載本文請註明作者和出處[Gary的影響力]http://garyelephant.me,請勿用於任何商業用途!
Author: Gary Gao 關注互聯網、分佈式、高併發、自動化、軟件團隊
支持我的工作: https://me.alipay.com/garygao