"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(), 不用isAutoCompletion()
- boundaryChecking()
- isOpenGLAvailable(), not openGL()
- isDialog(), not dialog()
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
绝大多数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进行改进的关键之处就是需要观察到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"这些词总会能用上一个.