工大后院

 找回密码
 加入后院

扫一扫,访问微社区

QQ登录

只需一步,快速开始

搜索
查看: 3543|回复: 1

[翻译]设计Qt式的C++ API

[复制链接]
发表于 2009-3-14 22:14 | 显示全部楼层 |阅读模式
原文:http://doc.trolltech.com/qq/qq13-apis.html

设计Qt式的C++ API


by Matthias Ettrich 译:iptton

    我们在Trolltech公司提高开发者体验方面进行了大量研究。这里,我想分享下其中部分以及QT4的设计准则,同时演示如何在你的代码中使用。

        * 良好API的六个特征
        * “方便”的陷阱(The Convenience Trap)
        * 布尔参数的陷阱
        * 静多态(Static Polymorphism)
        * 命名的技巧
        * 指针还是引用?
        * 实例研究: QProgressBar
        * 如何成就良好的API

设计应用程序接口(API)不是件容易的事情。它是有如设计一门编程语言一样困难的一种艺术,它有各种不同的原则供选择,而不同的原则又有些相互矛盾。

当今的计算机科学教育给与算法及数据结构很大的关注而对设计编程语言及框架背后的原则无甚讨论。这使得程序员们对日益增长的一个重要任务:组件重用性措手不及。

在对向对象语言兴起之前,可重用代码大部分是由程序库厂商开发而非应用程序程序员。在Qt社区,这种状态大大地改观了。用Qt写程序就是在编写新组件。一个传统的Qt程序至少都有一些被程序本身重用的自定义组件,通常同一些组件也被布署在其它应用程序里。KDE(K Desktop Environment),在这方面甚至走得更远,它扩展了Qt,添加了数以百计的类。

但是,一个好的有效的C++ API应该由什么组成呢?这个好与坏取决于几个因素,如当前进行的任务,目标用户组等。一个好的API应该有许多特性,一些是针对通用情况的而一些则是针对特定的问题域。

良好API的六个特征

API于程序员就如GUI于最终用户一样。API中的P表示Programmer而非Program,突出了API是给程序员,人类,使用的的事实。

我们坚信API应该是最小集合而又是全面的,语义清晰而简单的,可直觉感知的,易于记忆及便于写出易读的代码的。

    * 最小集合: 一个最小集合的API指有尽可能少的类及每个类里有尽可能少的公开成员(public member)。 这方便于理解,记忆,调试以及后期对API的修改。
    * 全面: 一个全面的API拥有应有的功能。这可能与保证最小集合有冲突。另外,如果一个成员函数被放在了不恰当的类里,许多该函数的潜在用户就有可能找不到它。
    * 语义清晰而简单: 在与其它设计一起时,你应保持最少的出人意料之外地使用本原则。(As with other design work, you should apply the principle of least surprise. )使得常见需求容易完成,罕见需求虽然也有可能出现但不应成为焦点。可以解决特殊问题,但在不必要时不要让它成为普遍的解决方案。(比如, Qt3 里的QMimeSourceFactory 应该称为QImageLoader并修改其API。)
    * 可直觉感知: 与计算机里任何东西一样,API应该是可直觉感知的。人们的认知能力与其经历背景相关联,而认知能力提供了对可直觉感知与否的判断。一个稍有经验的用户能够在无文档的情况下使用的API,并用程序员在不知道该API里可以读懂使用了该API的代码,那么这个就是可直觉感知的API。
    * 易于记忆: 要使API便于记忆,你需要使用一致而明确的命名方式,使用被认可的模式及概念并避免缩写。
    * 代码具易读性: 代码一次被写下来却要被阅读,测试及修改无数次。写出具有可读性的代码需要用更多的时间,便却为整个产品生产过程省下了时间。

最后,需要注意不同的用户会使用API的不同部分。 当简单地实例化Qt类是可直觉感知的的时候,我们有理由相信用户会在写Qt类的子类前会阅读文档。

“方便”的陷阱(The Convenience Trap)

用更少的代码达成某一目标的API就是好的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(),该函数有一个可选的 bool 类型参数,用于指定背景是否该被擦除(默认是被擦除)。这会带来以下代码:

    widget->repaint(false);
   

初学者在阅读时的理解可能是“不要重绘”!

添加 bool 参数的想法看起来减少了一个函数,避免了一定的代码臃肿度。但事实是它使得代码更加臃肿,能有多少个Qt用户完全地说出以下三行代码的意思?

    widget->repaint();
    widget->repaint(true);
    widget->repaint(false);
   

一个可能更好的API是这样的:

    widget->repaint();
    widget->repaintWithoutErasing();
   

我们在Qt4中通过不允许在重绘时不擦除原部件解决了这个问题。Qt4的本地支持提供了双缓存替代了该特性。

更多的例子:

    widget->setSizePolicy(QSizePolicy::Fixed,
                          QSizePolicy::Expanding, true);
    textEdit->insert("Where's Waldo?", true, true, false);
    QRegExp rx("moc_*.c??", false, true);
   

一个明显的解决方法是使用enum类型参数代替bool类型参数。比较以下看看我们在Qt4中对QString中的大小写敏感的做法:

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

静多态(Static Polymorphism)

类似的类应该有类似的API,这可以通过继承来实现--这就是运行时的多态。不过“多态”也会发生在设计阶段。例如:如果你将 QListBox 和 QComboBox,或者 a QSlider 和 QSpinBox 替换,你会发现它们的相似性使得这个替换变得极其容易。这就是我们所谓的“静多态(static polymorphism)”。

静多态也可以使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 为 true 。
    * textLabel 属性是在 QToolButton 里声明的,当 useTextLabel 为 true时显示其值。

为了易读性,Qt4的QToolButton 做了以下改动:name 被改成了 objectName , caption 被改成了 windowTitle, textLabel 则不复存在而仅明显地使用 text 属性。

类命名

为一组类命名而不要分开地为每个类命名。例如,所有的Qt4里的视图类都以 View 为后缀(QListView, QTableView 和 QTreeView );所有的基于item的类以 Widget 为后缀。(QListWidget, QTableWidget, and QTreeWidget).

枚举类型及其值命名

在声明枚举变量时,我们要关注其在C++上的特殊之处。与Java,C#不同, 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 变量里,这是类型不安全的(not type-safe)。Qt4提供了一个模板类 QFlags<T>, 此处 T 为枚举类型。 为了方便使用,Qt4为flag类型提供了预定义,因此你可以使用类型 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;
   

函数及参数命名

函数命名的首要原则是对函数是否有副作用表达清晰。Qt3 的 const函数 QString::simplifyWhiteSpace() 就违反了这条原则,从名字上猜,似乎是这样:函数返回一个 QString 而不是直接对原串修改。 Qt 4 中,些函数被重命名为 QString::simplified() 。

虽然参数名并不显示在使用了API的代码上,但是,对程序员来说,参数名是个获取信息的重要来源。现代集成开发环境通常会在程序员写代码时显示函数的参数,因此,在头文件里为参数提供一个恰当的名字并用于文档上是很有用的。

布尔成员值的设置/获取以及属性命名

一个getter是应该叫做 checked() 还是 isChecked() ,叫做 scrollBarsEnabled() 还是 areScrollBarEnabled() ?为 bool 类型函数命名通常是件痛苦的事件。

Qt4中,我们用以下方针命名getter函数:

    * 形容词用 is- 作前缀。例:
          o isChecked()
          o isDown()
          o isEmpty()
          o isMovingEnabled()
      但是作用于复数形式名词的形容词不加前缀:
          o 是scrollBarsEnabled(),而不是 areScrollBarsEnabled()
    * 动词不加前缀且不加第三人称的 (-s) 后缀:
          o 是acceptDrops(),而不是 acceptsDrops()
          o allColumnsShowFocus()
    * 名词一般不加前缀:
          o 是autoCompletion(),而不是 isAutoCompletion()
          o boundaryChecking()
      有时候,不加前缀会造成混淆,这时我们加上 is-作前缀:
          o 是isOpenGLAvailable(),而不是 openGL()
          o 是isDialog(),而不是 dialog()
      (调用函数 dialog()是,我们一般会期盼得到一个 QDialog *。)

setter函数的命名来自于其getter,用 set 前缀替换掉 is前缀。 如:setDown() 和 setScrollBarsEnabled()。除了 is 前缀外,属性的名字和getter一样。

指针还是引用?

指针和引用,哪个才是最好的参数?

    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 会在函数调用过程中修改。

实例研究: QProgressBar

这里通过Qt4和Qt3中的 QProgressBar API比较来描述在如何实践中的运用本文的观点。

    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() 的命名就不够清晰,三者是否紧密关联没表达出来。

改进该类的重点是注意到 QProgressBar 与Qt4的 QAbstractSpinBox 类及其了类 QSpinBox, QSlider 和 QDial 相似。 而解决方案则是用 minimum, maximum 和 value 替换 progress 和 totalSteps ,添加 valueChanged() 信号和 setRange() 函数。

另一个需要关注的地方是 progressString, percentage 和 indicator 。三者都指向processBar上显示的文字。通常processBar上的文字应该是百分比,但是它又可以通过函数 setIndicator() 设置成任意字符串。新的API如下:

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

默认情况下,processBar显示的是百分比。若想改变可覆盖实现 text() 函数。

Qt3中 setCenterIndicator() 和 setIndicatorFollowsStyle() 函数都可以改变对齐方式。这可以合并为一个函数 setAlignment():

    void setAlignment(Qt::Alignment alignment);
   

如果程序员没有调用 setAlignment() 函数,对齐方式将决定于窗口风格样式。 Motif风格下居中,其它风格下则居右。

改进后的 QProgressBar 接口:

    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,以及为API写文档,包括类概述及成员描述。

当你感到无从下手时,写文档也是一个找到好的命名的良好的途径。尝试为各部分(类,函数,枚举值等)写文档然后使用你对之的第一印象。 找不到准确的单词来命名通常意味着它不应该存在。如果以上都不行同时你坚信该概念是必须的,那你可以发明一个单词。想当初"widget","event","focus"和"buddy"就是这样来的。
发表于 2009-3-17 12:34 | 显示全部楼层
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 加入后院

本版积分规则

QQ|Archiver|手机版|小黑屋|广告业务Q|工大后院 ( 粤ICP备10013660号 )

GMT+8, 2024-5-12 12:47

Powered by Discuz! X3.5

Copyright © 2001-2024 Tencent Cloud.

快速回复 返回顶部 返回列表