原文链接:API Design Principles - QT Wiki
基于Gary的影响力上 Gary Gao 的译文稿:C++的API设计指导
🍎 译序
Qt的设计水准在业界很有口碑,一致、易于掌握和强大的API是Qt最著名的优点之一。此文既是Qt官网上的API设计指导准则,也是Qt在API设计上的实践总结。虽然Qt用的是C++,但其中设计原则和思考是具有普适性的(如果你对C++还不精通,可以忽略与C++强相关或是过于细节的部分,仍然可以学习或梳理关于API设计最有价值的内容)。整个篇幅中有很多示例,是关于API设计一篇难得的好文章。
API设计原则
一致、易于掌握和强大的API是Qt最著名的优点之一。此文总结了我们在设计Qt风格API的过程中所积累的诀窍(know-how)。其中许多是通用准则;而其他的则更偏向于约定,遵循这些约定主要是为了与已有的API保持一致。
虽然这些准则主要用于公有API(public API),但在设计私有API(private API)时也推荐遵循相同的技巧(techniques),作为开发者之间协作的礼仪(courtesy)。
如有兴趣也可以读一下 Jasmin Blanchette 的Little Manual of API Design (PDF) 或是本文的前身 Matthias Ettrich 的Designing Qt-Style C++ APIs。
1. 好API的6个特质
API之于程序员就如同GUI之于普通用户(end-user)。API中的『P』实际上指的是『程序员』(Programmer),而不是『程序』(Program),强调的是API是给程序员使用的这一事实。
在第13期Qt季刊,Matthias 的关于API设计的文章中提出了观点:API应该极简(minimal)且完备(complete)、语义清晰简单(have clear and simple semantics)、符合直觉(be intuitive)、易于记忆(be easy to memorize)和引导API使用者写出可读代码(lead to readable code)。
1.1 极简
极简的API是指每个公有类的公有成员尽可能少,公有类也尽可能少。这样的API更易理解、记忆、调试和变更。
1.2 完备
完备的API是指期望有的功能都包含了。这点会和保持API极简有些冲突。如果一个成员函数放到了错误的类里,那么这个函数的潜在用户就会找不到,这也是违反完备性的。
1.3 语义清晰简单
就像其他的设计一样,我们应该遵守最少意外原则(the principle of least surprise)。常见的任务应该很简单地完成,而对不常见的任务应该能完成且不至于很费心神。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在Qt 3中,QMimeSourceFactory不应命名成QImageLoader并有不一样的API。)
1.4 符合直觉
就像计算机里的其他事物一样,API应该符合直观。对于什么是符合直觉的什么不符合,不同的经验和背景的人会有不同的看法。API符合直观的测试方法:经验不很丰富的用户不用阅读API文档就能搞懂API,而且程序员不用了解API就能看明白使用API的代码。
1.5 易于记忆
为使API易于记忆,API的命名约定应该具有一致性和精确性。使用易于识别的模式和概念,并且避免用缩写。
1.6 引导API使用者写出可读代码
代码只写一次,却要多次的阅读(还有调试和修改)。写出可读性好的代码有时候要花费更多的时间,但对于产品的整个生命周期来说是节省了时间的。
最后,要记住的是,不同的用户会使用API的不同部分。尽管简单使用单个Qt类的实例应该符合直觉,但如果是要继承一个类,让用户事先看好文档是个合理的要求。
2. 静态多态
相似的类应该有相似的API。在继承(inheritance)合适时可以用继承达到这个效果,即运行时多态。然而多态也发生在设计阶段。例如,如果你用QProgressBar替换QSlider,或是用QString替换QByteArray,你会发现API的相似性使的替换很容易。这即是所谓的『静态多态』(static polymorphism)。
静态多态也使记忆API和编程模式更加容易。因此,一组相关的类有相似的API有时候比每个类都有各自的一套API更好。
一般来说,在Qt中,如果没有足够的理由要使用继承,我们更倾向于用静态多态。这样可以减少Qt公有类的个数,也使刚学习Qt的用户在翻看文档时更有方向感。
2.1 好的案例
QDialogButtonBox与QMessageBox,在处理按钮(addButton()、setStandardButtons()等等)上有相似的API,不需要继承某个QAbstractButtonBox类。
2.2 差的案例
QTcpSocket与QUdpSocket都继承了QAbstractSocket,这两个类的交互行为的模式(mode of interaction)非常不同。似乎没有什么人以通用和有意义的方式用过QAbstractSocket指针(或者 能 以通用和有意义的方式使用QAbstractSocket指针)。
2.3 值得斟酌的案例
QBoxLayout是QHBoxLayout与QVBoxLayout的基类。好处:可以在工具栏上使用QBoxLayout,调用setOrientation()使其变为水平/垂直。坏处:要多一个类,并且有可能导致用户写出这样没什么意义的代码,((QBoxLayout *)hbox)->setOrientation(Qt::Vertical)。
3. 基于属性的API
新的Qt类倾向于用『基于属性(property)的API』,例如:
QTimer timer;
timer.setInterval(1000);
timer.setSingleShot(true);
timer.start();
这里的 属性 是指任何的概念特征(conceptual attribute),是对象状态的一部分 —— 无论它是不是Q_PROPERTY。在说得通的情况下,用户应该可以以任何顺序设置属性,也就是说,属性之间应该是正交的(orthogonal)。例如,上面的代码可以写成:
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的例子,在不知道模式语法(pattern syntax)的情况下,在setPattern()中编译"."就为时过早了。
属性之间常常有关联的;在这种情况下,我们必须小心处理。思考下面的问题:当前的风格(style)提供了『默认的图标尺寸』属性 vs. QToolButton的『iconSize』属性:
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,设置就会一直保持,即使改变当前的风格。这 很好。但有的时候需要能重置属性。有两种方法:
- 传入一个特殊值(如
QSize()、-1或者Qt::Alignment(0))来表示『重置』 - 提供一个明确的重置方法,如
resetFoo()和unsetFoo()
对于iconSize,使用QSize()(比如 QSize(–1, -1))来表示『重置』就够用了。
在某些情况下,getter方法返回的结果与所设置的值不同。例如,虽然调用了widget->setEnabled(true),但如果它的父widget处于disabled状态,那么widget->isEnabled()仍然返回的是false。这样是OK的,因为一般来说就是我们想要的检查结果(父widget处于disabled状态,里面的子widget也应该变为灰的不响应用户操作,就好像子widget自身处于disabled状态一样;与此同时,因为子widget记得在自己的内心深处是enabled状态的,只是一直等待着它的父widget变为enabled)。当然诸如这些都必须在文档中妥善地说明清楚。
4. C++细节
4.1 值 vs. 对象
4.1.1 指针 vs. 引用
指针(pointer)还是引用(reference)哪个是最好的输出参数(out-parameters)?
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参数在函数调用中非常可能被修改。
4.1.2 按常量引用传递 vs. 按值传递
如果类型大于16字节,按常量引用传递。
如果类型有重型的(non-trivial)拷贝构造函数(copy-constructor)或是重型的析构函数(destructor),按常量引用传递以避免执行这些函数。
对于其它的类型通常应该按值传递。
示例:
void setAge(int age);
void setCategory(QChar cat);
void setName(QLatin1String name);
// const-ref is much faster than running copy-constructor and destructor
void setAlarm(const QSharedPointer<Alarm> &alarm);
// QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect
// are good examples of other classes you should pass by value.
4.2 虚函数
在C++中,当类的成员函数声明为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从Qt 3移植到Qt 4的时候,几乎所有的虚函数都被移除了。有趣的是(但在预料之中),并没有人对此有大的抱怨,为什么?因为Qt 3没用到QTextEdit的多态行为 —— 只有你会;简单得说,没有理由去继承QTextEdit并重新实现这些函数,除非你自己调用了这些方法。如果在Qt在外部你的应用程序你需要多态,你可以自己添加多态。
4.2.1 避免虚函数
在Qt中,我们有很多理由尽量减少虚函数的数量。每一次对虚函数的调用会在函数调用链路中插入一个未掌控的节点(某种程度上使结果更无法预测),使得bug修复变得更复杂。用户在重新实现的虚函数中可以做很多疯狂的事:
- 发送事件
- 送信号
- 重新进入事件循环(例如,通过打开一个模态文件对话框)
- 删除对象(即触发『
delete this』)
还有其他很多原因要避免过度使用虚函数:
- 添加、移动或是删除虚函数都带来二进制兼容问题(
binary binary/BC) - 重载虚函数并不容易
- 编译器几乎不能优化或内联(
inline)对虚函数的调用 - 虚函数调用需要查找虚函数表(
v-table),这比普通函数调用慢了2到3倍 - 虚函数使得类很难按值复制(尽管可能,但是非常混乱并且不建议这样做)
经验告诉我们,没有虚函数的类一般bug更少、维护成本也更低。
一般的经验法则是,除非我们以这个类作为工具包或是作为这个类的主要用户来调用函数,否则这个函数九成不应该设计成虚函数。
【TODO:工具包这句理解不了翻译得不清!】
4.2.2 虚函数 vs. 复制
多态对象(polymorphic objects)和值类型的类(value-type classes)两者很难协作好。
包含虚函数的类必须把析构函数声明为虚函数,以防止基类析构时没有清理子类的数据,导致内存泄漏。
如果要使一个类可以复制和赋值或者能按值比较,需要拷贝构造函数、赋值操作符(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这个类,预料之外的事就已经在代码时酝酿了。一般情况下,如果没有虚成员函数和虚析构函数,就不能有依赖多态的子类。然而,如果存在虚成员函数和虚析构函数,这突然变成了要有子类去继承的理由,而且开始变得复杂了。起初认为只要简单声明上虚操作符重载函数(virtual operators)。 但其实是走上了一条混乱和毁灭之路(看不明白?读作写出的是『不可读代码』)。看看下面的这个例子:
class OtherClass {
public:
const CopyClass &instance() const; // 这个方法返回的是什么?可以赋值什么?
};
(未完等续)
4.3 关于const
C++的关键词const表明了内容不会改变或是没有副作用。可以应用于简单的值、指针及指针所指的内容,也可以作为一个特别的属性应用于类的成员函数上,表示成员函数不能修改对象的状态。
然而,const本身并没有提供太大的价值 —— 很多编程语言甚至没有类似const的关键词,但是却并没有因此产生问题。实际上,如果你不用函数重载,并在C++源代码用搜索和替换来删除所有的const,几乎总能编译通过并且正常运行。尽量让使用的const保持实用有效,这点很重要。
让我们看一下在Qt的API设计中与const相关的场景。
4.3.1 输入参数:const指针
有输入指针参数的const成员函数,几乎总是const指针参数。
如果函数声明为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成员函数。注意,这些函数可以修改传入的参数,不能修改对象自己。使用这样的函数常常要借助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;
4.3.2 返回值:const值
调用函数返回的非引用类型的结果,称之为右值(R-value)。
非类(non-class)的右值总是无cv限定类型(cv-unqualified type)。虽然从语法上讲,加上const也可以,但是没什么意义,因为鉴于访问权限这些值是不能改变的。多数现代编译器在编译这样的代码时会提示警告信息。
【译注】:
cv-qualified的类型(与cv-unqualified相反)是由const或者volatile或者volatile const限定的类型。详见cv (const and volatile) type qualifiers -C++语言参考
当在类类型(class type)右值上添加const关键字,则禁止访问非const成员函数以及对成员的直接操作。
不加const则没有以上的限制,但几乎没有必要加上const,因为右值对象生存时间(life time)的结束一般在C++全清理(full-removed)的时候(通俗的说,下一个分号地方),而对右值对象的修改随着右值对象的生存时间就一起结束了。
【TODO:什么是full-removed ??需要调查理解!】
示例:
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
}
4.3.3 返回值:非const的指针还是有const的指针
谈到const函数应该返回非const的指针还是const指针这个话题时,多数人发现在C++中关于『const正确性』(const correctness)在概念上产生了分歧。 问题起源是:const函数本身不能修改对象自身的状态,却可以返回成员的非const指针。返回指针这个简单动作本身既不会影响整个对象的可见状态,当然也不会改变这个函数职责范围内涉及的状态。但是,这却使得程序员可以间接访问并修改对象的状态。
下面的例子演示了通过返回非const指针的const函数绕开const约定(constness)的诸多方式中的一种:
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指针,或是不止一个函数?
若采用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函数,顺着思考看起来这个函数只应该返回const指针。
在Qt中,我们几乎只有非const的使用模式。我们选择的是实用路子:
相比滥用非const指针返回类型带来的问题,返回const指针更可能招致过分使用const_cast的问题。
4.3.4 返回值:按值返回 还是 按const引用返回?
若返回的是对象的拷贝,那么返回const引用是更直接的方案;
然而,这样的做法限制了后面想要对这个类的重构(refactor)。
(以d-point的典型做法(idiom)为例,我们可以在任何时候改变Qt类在内存表示(memory representation);但却不能在不破坏二进制兼容性的情况下把改变函数的签名,返回值从const QFoo &变为QFoo。)
基于这个原因,除去对运行速度敏感(speed is critical)而重构不是问题的个别情形(例如,QList::at()),我们一般返回QFoo而不是const QFoo &。
4.4.5 const vs. 对象的状态
const正确性的问题就像C圈子中vi与emacs讨论,因为这个话题在很多地方都存在分歧(比如基于指针的函数)。
但通用准则是const函数不能改变类的可见状态。『状态』的意思是『自身以及涉及的职责』。这并不是指非const函数能够改变自身的私有成员,也不是指const函数改变不了。而是指函数是活跃的并存在可见的副作用(visible side effects)。const函数一般没有任何可见的副作用,比如:
QSize size = widget->sizeHint(); // const
widget->move(10, 10); // not const
代理(delegate)负责在其它对象上绘制内容。
它的状态包括它的职责,因此包括在哪个对象做绘制这样的状态。
调用它的绘画行为必然会有副作用;
它改变了它绘制所在设备的外观(及其所关联的状态)。鉴于这些,paint()作为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)
const关键字并不能按你期望的样子起作用。应该考虑将其移除而不是去重载const/非const函数。
5. API的语义和文档
当传值为-1的参数给函数,函数会是什么行为?有很多类似的问题……
是警告、致命错误还是其它?
API需要的是质量保证。API第一个版本一定是不对的;必须对其进行测试。
以阅读使用API的代码的方式编写用例,且验证这样代码是可读的。
还有其他的验证方法,比如
- 让别人使用
API(看了文档或是先不看文档都可以) - 给类写文档(包含类的概述和每个函数)
6. 命名的艺术
命名很可能是设计API时最简单最重要的方面。类应该用什么名字?成员函数应该用什么名字?
6.1 通用的命名规则
有几个规则对于所有类型的命名都等同适用。第一个,之前已经提到过,不要使用缩写。即使是明显的缩写,比如把previous缩写成prev,从长远来是回报是负的,因为用户必须要记住缩写词的含义。
如果API本身没有一致性,之后事情自然就会越来越糟;例如,Qt 3 中同时存在activatePreviousWindow()与fetchPrev()。恪守『不缩写』规则更容易地创建一致性的API。
另一个时重要但更微妙的准则是在设计类时应该保持子类名称空间的干净。在Qt 3中,此项准则没有被一直追随。以QToolButton为例对此进行说明。如果调用QToolButton的 name()、caption()、text()或者textLabel(),你觉得会返回什么?用Qt设计器在QToolButton上自己先试试吧:
name属性是继承自QObject,返回内部的对象名称,用于调试和测试。caption属性继承自QWidget,返回窗口标题,对QToolButton来说毫无意义,因为它在创建的时候parent就存在了。text函数继承自QButton,一般用于按钮。当useTextLabel不为true,才用这个属性。textLabel属性在QToolButton内声明,当useTextLabel为true时显示在按钮上。
为了可读性,name在Qt 4中改成了objectName,caption改为了windowTitle,QToolButton中再也没有textLabel了。
当你找不到好的名称时,开始写文档是一种好好的寻找方式:尝试为类、方法、枚举类型、值等写文档,把写下的第一句作为启发。如果找不确切的名称,这说明这个东西不该存在。如果所有尝试都失败了,并且你认为不如发明一个新名称,你就知道widget,event focus和buddy是如何产生的了。
6.2 类的命名
用把类的名称分组的方式替换为每个类单独命名的方法。例如,所有Qt 4的了解模型(model-aware)的视图(view)类后缀都是View(QListView、QTableView、QTreeView),相应的基于item的类后缀是Widget(QListWidget、QTableWidget、QTreeWidget)。
6.3 枚举类型及其值的命名
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型的值中,这不是类型安全的。Qt 4提供了一个模板类,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;
6.4 函数和参数的命名
函数命名的第一准则是可以从名称看出来此函数是否有副作用。在Qt 3中,QString::simplifyWhiteSpace()违反了此准则,因为它返回了一个QString而不是按名称暗示的那样,改变调用它的QString对象。在Qt 4中,此函数重命名为QString::simplified()。
虽然参数名称不会在使用API的代码中出现,但是它们给程序员提供了重要信息。因为现在的IDE都会在写代码时显示参数名称,所以应该在头文件中给参数起一个恰当的名称并在文档中使用相同的名称。
6.5 Boolean类型的getter与setter方法的命名
为bool属性的getter和setter方法命名总是很痛苦。getter应该叫做checked()还是isChecked()?scrollBarsEnabled()还是areScrollBarEnabled()?
Qt 4中,我们套用以下准则为getter命名:
- 形容词以
is为前缀,例子:isChecked()isDown()isEmpty()isMovingEnabled()
- 然而,修饰名词的形容词没有前缀:
scrollBarsEnabled(),而不是areScrollBarsEnabled()
- 动词没有前缀,也不使用第三人称(
-s):acceptDrops(),而不是acceptsDrops()allColumnsShowFocus()
- 名词一般没有前缀:
autoCompletion(),而不是isAutoCompletion()boundaryChecking()
- 有的时候,没有前缀容易产生误导,这种情况下会加上
is前缀:isOpenGLAvailable(),而不是openGL()isDialog(),而不是dialog()
(一个叫做dialog()的函数,一般会被认为是返回QDialog。)
setter的名称由getter衍生,去掉了前缀后在前面加上了set;例如,setDown()与setScrollBarsEnabled()。
7. 避免常见陷阱
7.1 简化的陷阱
一个常见的误解是:实现需要写的代码越少,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");
7.2 Boolean参数的陷阱
Boolean类型的参数总是带来无法阅读的代码。给现有的函数增加一个bool型的参数几乎永远是一种错误的行为。仍以Qt为例,repaint()有一个bool类型的可选参数用于指定背景是否被擦除。可以写出这样的代码:
widget->repaint(false);
初学者很可能是这样理解的,『不要重新绘制!』,能有多少Qt用户真心知道下面3行是什么意思:
widget->repaint();
widget->repaint(true);
widget->repaint(false);
更好的API设计应该是这样的:
widget->repaint();
widget->repaintWithoutErasing();
在Qt 4中,我们通过移除了重新绘制(repaint)而不擦除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
8. 案例研究
8.1 QProgressBar
为了展示上文各种准则的实际应用。我们来学习一下Qt 3中QProgressBar的API,并与Qt 4中对应的API作比较。
在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与Qt 4的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;
默认情况下,显示文本是百分比指示器(percentage indicator),重写text()方法来定制行为。
Qt 3的setCenterIndicator()与setIndicatorFollowsStyle()是两个影响对齐方式的函数。他们可被一个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);
...
};
8.2 QAbstractPrintDialog & QAbstractPageSizeDialog
Qt 4.0有2个幽灵类QAbstractPrintDialog和QAbstractPageSizeDialog,作为
QPrintDialog和QPageSizeDialog类的基类。这2个类完全没有用,因为QT的API没有是QAbstractPrint-或是-PageSizeDialog指针作为参数并执行操作。通过篡改qdoc(QT文档),我们把这2个类隐藏起来了,但却成了无用抽象类的典型案例。
这不是说,好 的抽象是错的,QPrintDialog应该是需要有个工厂或是其它改变的机制 —— 证据就是它声明中的#ifdef QTOPIA_PRINTDIALOG。
8.3 QAbstractItemModel
关于模型/视图(model/view)问题的细节在对应的文档中已经说明得很好了,但需要强调的一个重要的总结是:抽象类不应该仅仅是所有可能的子类的并集(union)。这样『合并所有』的抽象基类几乎不可能是一个好的方案。QAbstractItemModel就犯了这个错误 —— 它实际上就是个QTreeOfTablesModel,结果就导致了一个错综复杂(complicated)的API,而这样的API要让 所有本来设计还不错的子类 去继承。
仅仅增加抽象是不会自动就把API变得更好的。
8.4 QLayoutIterator & QGLayoutIterator
在QT 3,创建自定义的布局类需要同时继承QLayout和QGLayoutIterator(命名中的G是指Generic(通用))。QGLayoutIterator子类的实例指针会包装成QLayoutIterator,用户可以像其它的迭代器(iterator)类一样的使用。通过QLayoutIterator可以写出下面这样的代码:
QLayoutIterator it = layout()->iterator();
QLayoutItem **child;
while ((child = it.current()) != 0) {
if (child->widget() == myWidget) {
it.takeCurrent();
return;
}
++it;
}
在QT 4,我们干掉了QGLayoutIterator类(以及用于盒子布局和格子布局的内部子类),转而是让QLayout的子类重写itemAt()、takeAt()和count()。
8.5 QImageSink
Qt 3有一整套的类用来完成图片的增量加载后传递给一个动画 —— QImageSource/Sink/QASyncIO/QASyncImageIO。由于这些类之前只是用于启用动画的QLabel,完全过度设计了(overkill)。
从中得到的教训就是:对于那些未来可能的还不明朗的需求,不要过早地增加抽象设计。当需求真的出现时,比起一个复杂的系统,在简单的系统新增需求要容易得多。