设计Qt风格的C++API

这是转载的一篇翻译文章,作者翻译得非常好。读起来特别有感觉,因此转来保存。
原文网址1:http://www.cppblog.com/len/archive/2008/05/11/49563.html
原文网址2:http://www.cnblogs.com/zhuyf87/archive/2012/11/07/2758091.html

翻译这篇文章的目的不是让人了解Qt,而是让人试着学习点C++编程的软技能。我从原文中得到的一些风格上的体会,也希望你能从中有所收获.(译者注)
在奇趣(Trolltech),为了改进Qt的开发体验,我们做了大量的研究。这篇文章里,我打算分享一些我们的发现,以及一些我们在设计Qt4时用到的原则,并且展示如何把这些原则应用到你的代码里。

  • 优秀API的六个特性
  • 便利陷阱
  • 布尔参数陷阱
  • 静态多态
  • 命名的艺术
  • 指针还是引用?
  • 案例分析:QProgressBar
  • 如何把API设计好

设计应用程序接口(APIs)是有难度的,这是一门和设计语言同样难的艺术。要遵循许多不同的原则,这些原则中的许多还彼此冲突。
现在,计算机科学教育把很大的力气放在算法和数据结构上,而很少关注设计语言和框架背后的原则。这让应用程序员完全没有准备去面对越来越重要的任务:创造可重用的组件。
在面向对象语言普及之前,可重用的通用代码大部分是由库提供者写的,而不是应用程序员。在Qt的世界里,这种状况有了明显的改善。在任何时候,用Qt编程就是写新的组件。一个典型的Qt应用程序至少都会有几个在程序中反复使用的自定义组件。一般来说,同样的组件也会成为其他应用程序的一部分。KDE,K桌面环境,甚至使用了许多其它的附加库来扩展Qt,实现了数百个额外的类。
但是一个优秀的、高效的C++ API究竟是怎样的呢?它的好坏取决于许多因素,比如说,手头上的任务和特定目标群体。优秀的API具有很多特性,它们中的一些是普遍所要期望的,另一些是针对特定问题领域的。

优秀API的六个特性

API 对于程序员就相当于GUI对于最终用户。 API中“P”代表程序员(Programmer),而不是程序(Program),强调这一点是为了说明API是让程序员使用的,程序员是人而不机器。
我们认为APIs应当精简而完备,具有清晰简单的语义,直观、易记且应使代码具有可读性。
精简性: 精简的API具有尽可能少的类和公共成员。这使得理解,记忆,调试,更改API更加容易。
完备性: 是指要提供所有期望的功能。这可能与精简性原则相冲突。还有,如果成员函数放在不相匹配的类中,那么许多要使用这个功能函数的潜在用户会找不到它。
拥有清晰且简单的语义: 就像其他设计工作一样,你必须遵守最小惊奇原则(the principle of least surprise)。让常见的任务简单易行。不常见的工作可行,但不会让用户过分关注。解决特殊问题时,不要让解决方案没有必要的过度通用。(比如,Qt3中的QMimeSourceFactory可以通过调用QImageLoader来实现不同的API)。
直观性: 就像计算机上的其他东西一样,API应具有直观性。当然,不同的经验和背景可能会导致对哪些是直观,哪些不是直观有不同的看法。如果一个中级用户不读文档就可以使用(a semi-experienced user gets away without reading the documentation),并且对并不知晓这个API的程序员来说,他能够理解使用了这个API的代码,那么这个API就是具有直观性的。
易于记忆: 为了让API容易记忆,使用一致且精准的命名规范。使用容易识别的模式和概念,避免使用缩写。
引向易读的代码(Lead to readable code): 代码只写一遍,却要阅读许多遍(调试或更改)。易读的代码可能要多花点时间来写,但是从产品生命周期中可节省很多时间。
最后,记住,不同类型的用户会用到API的不同部分。虽然简单的实例化一个Qt类是非常直观的,但是期望用户在尝试子类化(扩展)之前阅读相关的文档还是合乎情理的。

便利陷阱

通常的误读是:越少的代码越能使你达到编写更好的API这一目的。请记住,代码只写一遍,却要一遍又一遍地去理解阅读它。比如:

 QSlider *slider = new QSlider(1218313, Qt::Vertical0"volume");

远比下面的代码难读(甚至难写):

 QSlider *slider = new QSlider(Qt::Vertical);
 slider->setRange(1218);
 slider->setPageStep(3);
 slider->setValue(13);
 slider->setObjectName("volume");

这可不是对OOD的KISS原则的违反。

布尔参数陷阱

布尔参数常常导致难以阅读的代码。特别地,增加某个bool参数到现存的函数一般都会是个错误的决定。在Qt中,传统的例子是repaint(),它带有一个可选的布尔参数,来指定背景是否删除(默认是删除)。这就导致了代码会像这样子:

widget->repaint(false);

初学者很容易把这句话理解成“不要重绘!”。
这样做是考虑到使用布尔参数可以减少一个函数,避免代码膨胀。事实上,这反而增加了代码量。有多少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);

一个显而易见的解决方法是,使用枚举类型代替布尔参数。这正是我们在Qt4中QString大小写敏感时的处理方法。比较下面两个例子:

str.replace("%USER%", user, false);               // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4

静态多态

相似的类应该有相似的API。在某种程度上,这能用继承来实现,也就是运用运行时多态机制。但是多态也能发生在API的设计阶段。比如,你将QListBox与QComboBox交换,QSlider与QSpinBox交换,你会发现API的相似性会使这种替换变得比较容易。这就是我们所谓的”静态多态”(STL的容器类就是这个原则的一个极佳例子)。
静态多态也能使记忆API和编程模式更加容易。作为结论,一组相关类使用相似的API,有时要比给每个类提供完美的单独API要好得多。

命名的艺术

命名有时候是设计API中最重要的事情了。某个类应叫什么名字,某个成员函数又应叫什么名字,都需要好好思考。
通用的命名规则
有少许规则对所有类型的命名都适用。首先,正如我早先所提到的,不要用缩写。甚至对用”prev”代表”previous”这样明显的缩写也不会在长期中受益,因为用户必须记住哪些名字是缩写。
如果API本身不一致,事情自然会变得很糟糕,比如, Qt3有activatePreviousWindow()和fetchPrev()。坚持“没有缩写”的规则会使创建一致的API变得更容易。
另一个重要但更加微妙的规则是,在设计类的时候,必须尽力保证子类命名空间的干净。在Qt3里,没有很好的遵守这个规则。比如,拿QToolButton来举例。如果你在Qt3里,对一个QToolButton调用name()、caption()、text()或者textLabel(),你希望做什么呢?你可以在Qt Designer里拿QToolButton试试:
name 属性继承自QObject,用来在调试和测试中指代对象的内部名称.
caption属性继承自QWidget,表示窗口的标题,这个标题在视觉上对QToolButton没有任何意义,因为他们总是跟随父窗口而创建。
text属性继承自QButton,一般情况下是按钮上显示的文字,除非useTextLabel为真。
textLabel在QToolButton里声明,并且在useTextLabel为真时显示在按钮上。
由于对可读性的关注,name在Qt4里被称作objectName,caption变成了windowsTitle,而在QToolButton里不再有单独的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保存,这不是类型安全的。Qt4提供了一个模板类 QFlags来实现类型安全,其中T是个枚举类型。为了方便使用,Qt为很多标志类名提供了typedef,所以你可以使用类型 Qt::Alignment代替QFlags。
为了方便,我们给枚举类型单数的名字(这样表示枚举值一次只能有一个标志),而“标志”则使用复数名字。比如:

enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef QFlags<RectangleEdge> RectangleEdges;

在某些情况下,”flags”类型有单数形式的名称。在这种情况下,枚举类型以Flag后缀标识:

enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef QFlags<AlignmentFlag> Alignment;

函数和参数的命名
给函数命名的一个规则是,名字要明确体现出这个函数是否有副作用。在Qt3,常数函数QString::simplifyWhiteSpace()违反了这个原则,因为它返回一个QString类实例,而不是像名字所提示的那样,更改了调用这个函数的实例本身。在Qt4,这个函数被重命名为QString::simplified()。
参数名对于程序员来说是重要的信息来源, 即使它们不出现在调用API 的代码中. 既然现代的IDE都会在程序员编码时显示这些参数,所以非常值得在头文件中给这些参数取恰当的名字,并且在文档中也使用相同的名字。
关于布尔型属性的getter/setter和属性的命名
给布尔属性的设置函数和提取函数取一个合适的名字,总是特别困难。提取函数应该叫做checked()还是isChecked()?scrollBarsEnabled()还是areScrollBarEnabled()?
在Qt4里,我们使用下列规则命名提取函数:
(1)形容词类的属性使用is-前缀。比如:
isChecked()
isDown()
isEmpty()
isMovingEnable()
另外,应用到复数名词的形容词类属性没有前缀:
scrollBarsEnabled(),而不是areScrollBarsEnabled()
(2)动词类的属性不使用前缀,且不使用第三人称(-s):
acceptDrops(),而不是acceptsDrops()
allColumnsShowFocus()
(3)名词类的属性,通常没有前缀:
autoCompletion(),而不是isAutoCompletion()
boundaryChecking()
当然,某些情况下没有前缀的话,就会引起误解,这种情况使用前缀is-:
isOpenGLAvailable(),而不是openGL()
isDialog(),而不是dialog()
(如果函数叫做dialog(),我们通常会认定它会返回QDialog*类型)
设置函数名字可以从提取函数名推出来,只是移掉了所有前缀,并使用set-做前缀,比如:setDown()还有setScrollBarsEnabled()。属性的名字与提取函数相同,只是去掉了“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相当复杂,且不统一。比如,仅从名字reset()并不能理解其作用,setTotalSteps()和setProgress()是紧耦合的。
改进API的关键,是注意到QProgressBar和Qt4的QAbstractSpinBox类及其子类QSpinBox,QSlider和QDial很相似。解决方法?用minimum、maximum和value代替progress和totalSteps。加入alueChanged()信号。加入setRange()函数。
之后观察progressString、percentage和indicator实际都指一个东西:在进度条上显示的文字。一般来说文字是百分比信息,但是也可以使用setIndicator()设为任意字符。下面是新的API:

virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;

默认的文字信息是百分比信息。文字信息可以藉由重新实现text()而改变。
在Qt3 API中,setCenterIndicator()和setIndicatorFollowStyle()是两个影响对齐的函数。他们可以方便的由一个函数实现,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);
    ...
};

如何把API设计好

API需要质量保证。最早的版本一般都不是很好的,你必须测试它。写些用例:看看那些使用了这些API的代码,并验证代码是否易读。
其他的技巧包括让别人分别在有文档和没有文档的情况下,使用这些API。记住要为API类写文档(包括类的概述和独立的函数)。
当你卡住时,写文档也是一种获得好名字的方法:仅仅是尝试把条目(类,函数,枚举值,等等)写下来,并且使用你写的第一句话作为灵感。如果你还是不能找到一 个精确的名字,这常常说明这个概念不应该存在。如果所有前面的事情都失败了并且你确认这个概念应该存在,那么尝试发明一个新名字。毕竟,“widget”、 “event”、“focus”和“buddy”这些名字就是这么来的。

你可能感兴趣的:(api,qt)