软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> 研发管理 -> 设计Qt风格的C++API -> 正文阅读

[研发管理]设计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(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#),枚举值在使用时不带类型名。下面的例子说明了对枚举值取太一般化的名字的危害:
[img]http://common.cnblogs.com/images/copycode.gif
namespace Qt
{
    enum Corner { TopLeft, BottomRight, ... };
    enum CaseSensitivity { Insensitive, Sensitive };
    ...
};

tabWidget->setCornerWidget(widget, Qt::TopLeft);

str.indexOf("$(QTDIR)", Qt::Insensitive);

[img]http://common.cnblogs.com/images/copycode.gif
在最后一行,Insensitive是什么意思?一个用于命名枚举值的指导思想是,在每个枚举值里,至少重复一个枚举类型名中的元素:
[img]http://common.cnblogs.com/images/copycode.gif
namespace Qt
{
    enum Corner { TopLeftCorner, BottomRightCorner, ... };
    enum CaseSensitivity { CaseInsensitive,
                           CaseSensitive };
    ...
};

tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);

[img]http://common.cnblogs.com/images/copycode.gif
当枚举值可以用“或”连接起来当作一个标志时,传统的做法是将“或”的结果作为一个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中:
[img]http://common.cnblogs.com/images/copycode.gif
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);
    ...
};

[img]http://common.cnblogs.com/images/copycode.gif
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:
[img]http://common.cnblogs.com/images/copycode.gif
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);
    ...
};

[img]http://common.cnblogs.com/images/copycode.gif
如何把API设计好
API需要质量保证。第一个修订版不可能是正确的;你必须做测试。写些用例:看看那些使用了这些API的代码,并验证代码是否易读。
其他的技巧包括让别人分别在有文档和没有文档的情况下,使用这些API并且为API类写文档(包括类的概述和独立的函数)。
当你卡住时,写文档也是一种获得好名字的方法:仅仅是尝试把条目(类,函数,枚举值,等等呢个)写下来并且使用你写的第一句话作为灵感。如果你不能找到一 个精确的名字,这常常说明这个条目不应该存在。如果所有前面的事情都失败了并且你确认这个概念的存在,发明一个新名字。毕竟,“widget”、 “event”、“focus”和“buddy”这些名字就是这么来的。
......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-04-01 16:58:54  
研发管理 最新文章
拉格朗日乘数
maven之可视化项目依赖(Visualizingdepend
mac效率工具
Atitit.css规范bem项目中CSS的组织和管理
git入门
Asimplemodelfordescribingbasicsourcesofp
Linux进程管理浅析
我的openwrt学习笔记(十九):linux便捷开
2、微控制器选择
Git使用手册:为Git仓库创建Submodule
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年1日历
2018-1-17 9:22:29
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --