zoukankan      html  css  js  c++  java
  • 设计Qt风格的C++API

    在奇趣(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(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);

    初学者很容易把这句话理解成“别重画”!

    这样做是考虑到使用布尔参数可以减少一个函数,避免代码膨胀。事实上,这反而增加了代码量。有多少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”这些名字就是这么来的。

    https://blog.csdn.net/u011822862/article/details/51017510

  • 相关阅读:
    LabVIEW入门第六天(布尔控件及布尔量)
    LabVIEW如何加载显示GIF动画
    C#打开EXCEL或保存文件时报错:System.InvalidOperationException:未在本地计算机上注册” Microsoft.ACE.OLEDB.12.0“提供程序。...
    SQL 怎么设置自定义服务器名称登录
    C#打开EXCEL或保存文件时报错:System.InvalidOperationException:未在本地计算机上注册” Microsoft.ACE.OLEDB.12.0"提供程序。
    半平面交
    poj1038
    poj2318 && poj2398
    poj1696
    poj2430
  • 原文地址:https://www.cnblogs.com/findumars/p/11172622.html
Copyright © 2011-2022 走看看