设计Qt风格的C++API

原文地址:http://doc.qt.digia.com/qq/qq13-apis.html

在奇趣(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::Vertical,
                                  0"volume");

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

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

布尔参数陷阱

布尔参数常常导致难以阅读的代码。特别地,增加某个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。在某种程度上,这能用继承来实现,也就是运用运行时多态机制。但是多态也可以发生在设计时期。比如,如果你用QListBox代替QComboBox,或者用QSlider代替QSpinBox,你会发现相似的API使这种替换非常容易。这就是我们所说的“静态多态”。

静态多态也使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属性。

给类命名

标识一组类而不是单独给每个类找个恰当的名字。比如,Qt4里所有模式感知项目的视图类(model-aware item view classes)都拥有-View的后缀(QListView、QTableView和QTreeView),并且对基于部件的类都用后缀-Widget代替(QListWidget、QTableWidget和QTreeWidget)。

枚举类型和值类型命名

当设计枚举时,我们应当记住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>来实现类型安全,其中T是个枚举类型。为了方便使用,Qt为很多标志类名提供了typedef,所以你可以使用类型 Qt::Alignment代替QFlags<Qt::AlignmentFlag>。

为了方便,我们给枚举类型单数的名字(这样表示枚举值一次只能有一个标志),而“标志”则使用复数名字。比如:

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会在程序员编码时显示这些参数,所以非常值得在头文件中给这些参数取恰当的名字,并且在文档中也使用相同的名字。

给布尔值设置函数(Setter)、提取函数(Getter)和属性命名

给布尔属性的设置函数和提取函数取一个合适的名字,总是特别困难的。提取函数应该叫做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”这些名字就是这么来的。

Reference:

[1]: http://my.oschina.net/qihh/blog/56686

[2]: http://www.cppblog.com/len/archive/2008/05/11/49563.html#sixcharacteristicsofgoodapis

 

 

posted on 2012-11-07 07:38  zhuyf87  阅读(3247)  评论(0编辑  收藏  举报

导航