子弹 の VISIONS

NEVER back down ~~

C++博客 首页 新随笔 联系 聚合 管理
  112 Posts :: 34 Stories :: 99 Comments :: 0 Trackbacks
Designing Qt-Style C++ APIs
by Matthias Ettrich
We have done substantial research at Trolltech into improving the Qt development experience. In this article, I want to share some of our findings and present the principles we've been using when designing Qt 4, and show you how to apply them to your code.

 

Designing application programmer interfaces, APIs, is hard. It is an art as difficult as designing programming languages. There are many different principles to choose from, many of which tend to contradict each other.

Computer science education today puts a lot of emphasis on algorithms and data structures, with less focus on the principles behind designing programming languages and frameworks. This leaves application programmers unprepared for an increasingly important task: the creation of reusable components.

Before the rise of object-oriented languages, reusable generic code was mostly written by library vendors rather than by application developers. In the Qt world, this situation has changed significantly. Programming with Qt is writing new components all the time. A typical Qt application has at least some customized components that are reused throughout the application. Often the same components are deployed as part of other applications. KDE, the K Desktop Environment, goes even further and extends Qt with many add-on libraries that implement hundreds of additional classes.

But what constitutes a good, efficient C++ API? What is good or bad depends on many factors -- for example, the task at hand and the specific target group. A good API has a number of features, some of which are generally desirable, and some of which are more specific to certain problem domains.

Six Characteristics of Good APIs

An API is to the programmer what a GUI is to the end-user. The 'P' in API stands for "Programmer", not "Program", to highlight the fact that APIs are used by programmers, who are humans.

We believe APIs should be minimal and complete, have clear and simple semantics, be intuitive, be easy to memorize, and lead to readable code.

Finally, keep in mind that different kinds of users will use different parts of the API. While simply using an instance of a Qt class should be intuitive, it's reasonable to expect the user to read the documentation before attempting to subclass it.

 

The Convenience Trap

It is a common misconception that the less code you need to achieve something, the better the API. Keep in mind that code is written more than once but has to be understood over and over again. For example,

    QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical,
0, "volume");

is much harder to read (and even to write) than

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

 

The Boolean Parameter Trap

Boolean parameters often lead to unreadable code. In particular, it's almost invariably a mistake to add a bool parameter to an existing function. In Qt, the traditional example is repaint(), which takes an optional bool parameter specifying whether the background should be erased (the default) or not. This leads to code such as

    widget->repaint(false);

which beginners might read as meaning, "Don't repaint!"

The thinking is apparently that the bool parameter saves one function, thus helping reducing the bloat. In truth, it adds bloat; how many Qt users know by heart what each of the next three lines does?

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

A somewhat better API might have been

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

In Qt 4, we solved the problem by simply removing the possibility of repainting without erasing the widget. Qt 4's native support for double buffering made this feature obsolete.

Here come a few more examples:

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

An obvious solution is to replace the bool parameters with enum types. This is what we've done in Qt 4 with case sensitivity in QString. Compare:

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

 

Static Polymorphism

Similar classes should have a similar API. This can be done using inheritance where it makes sense -- that is, when run-time polymorphism is used. But polymorphism also happens at design time. For example, if you exchange a QListBox with a QComboBox, or a QSlider with a QSpinBox, you'll find that the similarity of APIs makes this replacement very easy. This is what we call "static polymorphism".

Static polymorphism also makes it easier to memorize APIs and programming patterns. As a consequence, a similar API for a set of related classes is sometimes better than perfect individual APIs for each class.

The Art of Naming

Naming is probably the single most important issue when designing an API. What should the classes be called? What should the member functions be called?

General Naming Rules

A few rules apply equally well to all kinds of names. First, as I mentioned earlier, do not abbreviate. Even obvious abbreviations such as "prev" for "previous" don't pay off in the long run, because the user must remember which words are abbreviated.

Things naturally get worse if the API itself is inconsistent; for example, Qt 3 has activatePreviousWindow() and fetchPrev(). Sticking to the "no abbreviation" rule makes it simpler to create consistent APIs.

Another important but more subtle rule when designing classes is that you should try to keep the namespace for subclasses clean. In Qt 3, this principle wasn't always followed. To illustrate this, we will take the example of a QToolButton. If you call name(), caption(), text(), or textLabel() on a QToolButton in Qt 3, what do you expect? Just try playing around with a QToolButton in Qt Designer:

  • The name property is inherited from QObject and refers to an internal object name that can be used for debugging and testing.
  • The caption property is inherited from QWidget and refers to the window title, which has virtually no meaning for QToolButtons, since they usually are created with a parent.
  • The text property is inherited from QButton and is normally used on the button, unless useTextLabel is true.
  • The textLabel property is declared in QToolButton and is shown on the button if useTextLabel is true.
In the interest of readability, name is called objectName in Qt 4, caption has become windowTitle, and there is no longer any textLabel property distinct from text in QToolButton.

 

Naming Classes

Identify groups of classes instead of finding the perfect name for each individual class. For example, All the Qt 4 model-aware item view classes are suffixed with View (QListView, QTableView, and QTreeView), and the corresponding item-based classes are suffixed with Widget instead (QListWidget, QTableWidget, and QTreeWidget).

Naming Enum Types and Values

When declaring enums, we must keep in mind that in C++ (unlike in Java or C#), the enum values are used without the type. The following example shows illustrates the dangers of giving too general names to the enum values:

    namespace Qt
{
enum Corner { TopLeft, BottomRight, ... };
enum CaseSensitivity { Insensitive, Sensitive };
...
};
tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);

In the last line, what does Insensitive mean? One guideline for naming enum types is to repeat at least one element of the enum type name in each of the enum values:

    namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, ... };
enum CaseSensitivity { CaseInsensitive,
CaseSensitive };
...
};
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);

When enumerator values can be OR'd together and be used as flags, the traditional solution is to store the result of the OR in an int, which isn't type-safe. Qt 4 offers a template class QFlags<T>, where T is the enum type. For convenience, Qt provides typedefs for the flag type names, so you can type Qt::Alignment instead of QFlags<Qt::AlignmentFlag>.

By convention, we give the enum type a singular name (since it can only hold one flag at a time) and the "flags" type a plural name. For example:

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

In some cases, the "flags" type has a singular name. In that case, the enum type is suffixed with Flag:

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

Naming Functions and Parameters

The number one rule of function naming is that it should be clear from the name whether the function has side-effects or not. In Qt 3, the const function QString::simplifyWhiteSpace() violated this rule, since it returned a QString instead of modifying the string on which it is called, as the name suggests. In Qt 4, the function has been renamed QString::simplified().

Parameter names are an important source of information to the programmer, even though they don't show up in the code that uses the API. Since modern IDEs show them while the programmer is writing code, it's worthwhile to give decent names to parameters in the header files and to use the same names in the documentation.

Naming Boolean Getters, Setters, and Properties

Finding good names for the getter and setter of a bool property is always a special pain. Should the getter be called checked() or isChecked()? scrollBarsEnabled() or areScrollBarEnabled()?

In Qt 4, we used the following guidelines for naming the getter function:

  • Adjectives are prefixed with is-. Examples:
    • isChecked()
    • isDown()
    • isEmpty()
    • isMovingEnabled()
    However, adjectives applying to a plural noun have no prefix:
    • scrollBarsEnabled(), not areScrollBarsEnabled()
  • Verbs have no prefix and don't use the third person (-s):
    • acceptDrops(), not acceptsDrops()
    • allColumnsShowFocus()
  • Nouns generally have no prefix:
    • autoCompletion(), not isAutoCompletion()
    • boundaryChecking()
    Sometimes, having no prefix is misleading, in which case we prefix with is-:
    • isOpenGLAvailable(), not openGL()
    • isDialog(), not dialog()
    (From a function called dialog(), we would normally expect that it returns a QDialog *.)
The name of the setter is derived from that of the getter by removing any is prefix and putting a set at the front of the name; for example, setDown() and setScrollBarsEnabled(). The name of the property is the same as the getter, but without the is prefix.

 

Pointers or References?

Which is best for out-parameters, pointers or references?

    void getHsv(int *h, int *s, int *v) const
void getHsv(int &h, int &s, int &v) const

Most C++ books recommend references whenever possible, according to the general perception that references are "safer and nicer" than pointers. In contrast, at Trolltech, we tend to prefer pointers because they make the user code more readable. Compare:

    color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);

Only the first line makes it clear that there's a high probability that h, s, and v will be modified by the function call.

Case Study: QProgressBar

To show some of these concepts in practice, we'll study the QProgressBar API of Qt 3 and compare it to the Qt 4 API. In 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);
...
};

The API is quite complex and inconsistent; for example, it's not clear from the naming that reset(), setTotalSteps(), and setProgress() are tightly related.

The key to improve the API is to notice that QProgressBar is similar to Qt 4's QAbstractSpinBox class and its subclasses, QSpinBox, QSlider and QDial. The solution? Replace progress and totalSteps with minimum, maximum and value. Add a valueChanged() signal. Add a setRange() convenience function.

The next observation is that progressString, percentage and indicator really refer to one thing: the text that is shown on the progress bar. Usually the text is a percentage, but it can be set to anything using the setIndicator() function. Here's the new API:

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

By default, the text is a percentage indicator. This can be changed by reimplementing text().

The setCenterIndicator() and setIndicatorFollowsStyle() functions in the Qt 3 API are two functions that influence alignment. They can advantageously be replaced by one function, setAlignment():

    void setAlignment(Qt::Alignment alignment);

If the programmer doesn't call setAlignment(), the alignment is chosen based on the style. For Motif-based styles, the text is shown centered; for other styles, it is shown on the right hand side.

Here's the improved 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);
...
};

 

How to Get APIs Right

APIs need quality assurance. The first revision is never right; you must test it. Make use cases by looking at code which uses this API and verify that the code is readable.

Other tricks include having somebody else use the API with or without documentation and documenting the class (both the class overview and the individual functions).

Documenting is also a good way of finding good names when you get stuck: just try to document the item (class, function, enum value, etc.) and use your first sentence as inspiration. If you cannot find a precise name, this is often a sign that the item shouldn't exist. If everything else fails and you are convinced that the concept makes sense, invent a new name. This is, after all, how "widget", "event", "focus", and "buddy" came to be.

posted on 2008-05-30 16:51 子弹のVISIONS 阅读(777) 评论(1)  编辑 收藏 引用 所属分类: 2.0 工作参考

Feedback

# re: Designing Qt-Style C++ APIs 2008-05-30 16:52 子弹

设计Qt风格的C++API

作者Matthias Ettrich,译者Googol Lee,原文地址在这里

在奇趣(Trolltech),为了改进Qt的开发体验,我们做了大量的研究。这篇文章里,我打算分享一些我们的发现,以及一些我们在设计Qt4时用到的原则,并且展示如何把这些原则应用到你的代码里。

设计应用程序接口,API,是很难的。这是一门和设计语言同样难的艺术。这里可以选择太多的原则,甚至有很多原则和其他原则有矛盾。

现在,计算机科学教育把很大的力气放在算法和数据结构上,而很少关注设计语言和框架背后的原则。这让应用程序员完全没有准备去面对越来越重要的任务:创造可重用的组件。

在面向对象语言普及之前,可重用的通用代码大部分是由库提供者写的,而不是应用程序员。在Qt的世界里,这种状况有了明显的改善。在任何时候,用Qt编程就是写新的组件。一个典型的Qt应用程序至少都会有几个在程序中反复使用的自定义组件。一般来说,同样的组件会成为其他应用程序的一部分。KDE,K桌面环境,走得更远,用许多追加的库来扩展Qt,实现了数百个附加类。(一般来说,一个类就是一个可重用组件,原文这里没有写清楚。)

但是,一个好的,高效的C++ API是由什么组成的呢?是好还是坏,取决于很多因素——比如,手头的工作和特定的目标群体。好的API有很多特性,一些特性是大家都想要的,而另一些则是针对特定问题域的。

好的API的六个特性

API是面向程序员的,用来描述提供给最终用户的GUI是什么样子。API中的P带表程序员(Programmer),而不是程序(Program),用来强调API是给程序员用的,给人类的程序员用的。

我们坚信API应该是最小化且完整的,拥有清晰且简单的语义,直觉化,容易记忆,并且引导人写出易读的代码。

  • 最小化:最小化的API是指一个类尽可能只拥有最少的公开成员且尽可能只拥有最少的类。这个原则可以让API更简单易懂,更好记,更容易除错,且更容易改变。
  • 完整的:完整的API是指要提供所有期望的功能。这个可能与最小化原则相冲突。另外,如果一个成员函数属于一个不应该属于的类,很多潜在的使用者都会找不到这个函数。
  • 拥有清晰且简单的语义:就像其他设计工作一样,你必须遵守最小惊奇原则(the principle of least surprise)。让常见的任务简单易行。不常见的工作可行,但不会让用户过分关注。解决特殊问题时,不要让解决方案没有必要的过度通用。(比如,Qt3中的QMimeSourceFactory可以通过调用QImageLoader来实现不同的API。)
  • 直觉化:就像电脑上的其他东西一样,API必须是直觉化的。不同的经验和背景会导致在判断什么是直觉而什么不是时不同的感觉。如果一个中级用户不读文档就可以使用(a semi-experienced user gets away without reading the documentation,没懂这里的get away该怎么翻译),并且一个程序员不懂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");

布尔参数陷阱

布尔参数通常会导致不易读的代码。更进一步,给一个已经存在的函数加入一个布尔参数,这常常是个错误。在Qt里,一个传统的例子是repaint(),这个函数带有一个布尔参数,来标识是否擦除背景(默认擦除)。这让代码通常写成:

    widget->repaint(false);

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

这样做是考虑到布尔参数可以减少一个函数,避免代码膨胀。事实上,这反而增加了代码量。有多少Qt用户真的记住了下面三行程序都是做什么的?

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

一个好一些的API可能看起来是这样:

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

在Qt4里,我们重新设计了widget,使得用户不再需要不重画背景的重画widget,来解决这个问题。Qt4原生支持双缓存,废掉了这个特性。

这里还有一些例子:

    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,要好。

(译注:C++ 0x将要引入的concept,就是静态多态的语法层实现。这个要比单独的函数名相似更强大且易用。)

命名的艺术

命名,大概是设计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的后缀(QListViewQTableViewQTreeView),并且对应基于项目的类都用后缀-Widget代替(QListWidgetQTableWidgetQTreeWidget)。

给枚举类型及其值命名

当声明枚举时,时刻记住,在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;

有些情况下,“标志“类使用了单数的名字。这时,枚举类使用-Flag做后缀:

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

(这里为啥不是把”标志“类用-Flag做后缀,而是把枚举值做后缀呢?感觉有点混淆……)

给函数和参数命名

给函数命名的一个规则是,名字要明确体现出这个函数是否有副作用。在Qt3,常数函数QString::simplifyWhiteSpace()违反了这个原则,因为它返回类一个QString实例,而不是像名字所提示的那样,更改了调用这个函数的实例本身。在Qt4,这个函数被重命名为QString::simplified()。

参数名是程序员的重要信息来源,虽然在使用API时,并不直接展示在代码里。由于现代IDE在程序员写代码时可以自动显示参数名(就是自动感知或者自动补全之类的功能),值得花时间给头文件里声明的参数一个合适的名字,并且在文档中也使用相同的名字。

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

给布尔属性的设置函数和提取函数一个合适的名字,总是非常痛苦的。提取函数应该叫做checked()还是isChecked()?scrollBarsEnabled()还是areScrollBarEnabled()?

在Qt4里,我们使用下列规则命名提取函数:

  • 形容类的属性使用is-前缀。比如:
    • isChecked()
    • isDown()
    • isEmpty()
    • isMovingEnable()
    另外,应用到复数名词的形容类属性没有前缀:
    • scrollBarsEnabled(),而不是areScrollBarsEnabled()
  • 动词类的属性不使用前缀,且不使用第三人称(-s):
    • acceptDrops(),而不是acceptsDrops()
    • allColumnsShowFocus()
  • 名词类的属性,通常没有前缀:
    • autoCompletion(),而不是isAutoCompletion()
    • boundaryChecking()
    有时,没有前缀就会引起误解,这种情况使用前缀is-:
    • isOpenGLAvailable(),而不是openGL()
    • isDialog(),而不是dialog()
    (通过调用dialogue()方法,正常情况下会期望返回一个QDialog*的实例。)

设置函数名字继承自提取函数名,只是移掉了所有前缀,并使用set-做前缀,比如:setDown()还有setScrollBarsEnabled()。属性的名字与提取函数相同,只是去掉了前缀。

指针还是引用?

传出参数的最佳选择是什么,指针还是引用?

    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

为了展示如何实际应用这些概念,我们将学习Qt3中的API QProgressBar并和Qt4里相通的API做比较。在Qt3里:

    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类及其子类QSpinBoxQSliderQDial很相似。解决方法?用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设计好(原文是How to Get APIs Right,我总想成We do APIs right……)

API需要质量保证。第一个修订版不可能是正确的;你必须做测试。写些用例:看看那些使用了这些API的代码,并验证代码是否易读。

其他的技巧包括让别的人分别在有文档和没有文档的情况下,使用这些API;或者为API类写文档(包括类的概述和独立的函数)。

当你卡住时,写文档也是一种获得好名字的方法:仅仅是尝试把条目(类,函数,枚举值,等等呢个)写下来并且使用你写的第一句话作为灵感。如果你不能找到一个精确的名字,这常常说明这个条目不应该存在。如果所有前面的事情都失败了并且你确认这个概念的存在,发明一个新名字。毕竟,“widget”、 “event”、“focus”和“buddy”这些名字就是这么来的。

  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理