diff --git a/api-design-principles-from-qt/README.md b/api-design-principles-from-qt/README.md index 1d38915..0a4d15a 100644 --- a/api-design-principles-from-qt/README.md +++ b/api-design-principles-from-qt/README.md @@ -5,11 +5,13 @@ `Qt`的设计水准在业界很有口碑,一致、易于掌握和强大的`API`是`Qt`最著名的优点之一。此文既是`Qt`官网上的`API`设计指导准则,也是`Qt`在`API`设计上的实践总结。虽然`Qt`用的是`C++`,但其中设计原则和思考是具有普适性的(如果你对`C++`还不精通,可以忽略与`C++`强相关或是过于细节的部分,仍然可以学习或梳理关于`API`设计最有价值的内容)。整个篇幅中有很多示例,是关于`API`设计一篇难得的好文章。 +需要注意的是,这篇 Wiki 有一些东西并不完整,所以,可能会有一些阅读上的问题。我们对此做了一些相关的注释。 + # `API`设计原则 一致、易于掌握和强大的`API`是`Qt`最著名的优点之一。此文总结了我们在设计`Qt`风格`API`的过程中所积累的诀窍(`know-how`)。其中许多是通用准则;而其他的则更偏向于约定,遵循这些约定主要是为了与已有的`API`保持一致。     -虽然这些准则主要用于公有`API`(`public API`),但在设计私有`API`(`private API`)时也推荐遵循相同的技巧(`techniques`),作为开发者之间协作的礼仪(`courtesy`)。 +虽然这些准则主要用于对外的`API`(`public API`),但在设计对内的`API`(`private API`)时也推荐遵循相同的技巧(`techniques`),作为开发者之间协作的礼仪(`courtesy`)。 如有兴趣也可以读一下 _Jasmin Blanchette_ 的[Little Manual of API Design (PDF)](http://www4.in.tum.de/~blanchet/api-design.pdf) 或是本文的前身 _Matthias Ettrich_ 的[Designing Qt-Style C++ APIs](https://doc.qt.io/archives/qq/qq13-apis.html)。 @@ -69,13 +71,13 @@ # 1. 好`API`的6个特质 -`API`之于程序员就如同`GUI`之于普通用户(`end-user`)。`API`中的『`P`』实际上指的是『程序员』(`Programmer`),而不是『程序』(`Program`),强调的是`API`是给程序员使用的这一事实。 +`API`之于程序员就如同图形界面之于普通用户(`end-user`)。`API`中的『`P`』实际上指的是『程序员』(`Programmer`),而不是『程序』(`Program`),强调的是`API`是给程序员使用的这一事实。 在第13期[`Qt`季刊](http://doc.qt.io/archives/qq/),_Matthias_ 的[关于`API`设计的文章](https://doc.qt.io/archives/qq/qq13-apis.html)中提出了观点:`API`应该极简(`minimal`)且完备(`complete`)、语义清晰简单(`have clear and simple semantics`)、符合直觉(`be intuitive`)、易于记忆(`be easy to memorize`)和引导`API`使用者写出可读代码(`lead to readable code`)。 ## 1.1 极简 -极简的`API`是指每个公有类的公有成员尽可能少,公有类也尽可能少。这样的`API`更易理解、记忆、调试和变更。 +极简的`API`是指每个 class 的 public 成员尽可能少,public 的 class 也尽可能少。这样的`API`更易理解、记忆、调试和变更。 ## 1.2 完备 @@ -83,11 +85,11 @@ ## 1.3 语义清晰简单 -就像其他的设计一样,我们应该遵守最少意外原则(`the principle of least surprise`)。常见的任务应该很简单地完成,而对不常见的任务应该能完成且不至于很费心神。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在`Qt 3`中,`QMimeSourceFactory`不应命名成`QImageLoader`并有不一样的`API`。) +就像其他的设计一样,我们应该遵守最少意外原则(`the principle of least surprise`)。好的API应该可以让常见的事完成的更简单,并有可以完成不常见的事的可能性,但是却不会关注于那些不常见的事。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在`Qt 3`中,`QMimeSourceFactory`不应命名成`QImageLoader`并有不一样的`API`。) ## 1.4 符合直觉 -就像计算机里的其他事物一样,`API`应该符合直观。对于什么是符合直觉的什么不符合,不同的经验和背景的人会有不同的看法。`API`符合直观的测试方法:经验不很丰富的用户不用阅读`API`文档就能搞懂`API`,而且程序员不用了解`API`就能看明白使用`API`的代码。 +就像计算机里的其他事物一样,`API`应该符合直观。对于什么是符合直觉的什么不符合,不同的经验和背景的人对是否符俣直觉有不同的看法。`API`符合直观的测试方法:经验不很丰富的用户不用阅读`API`文档就能搞懂`API`,而且程序员不用了解`API`就能看明白使用`API`的代码。 ## 1.5 易于记忆 @@ -105,7 +107,7 @@ 静态多态也使记忆`API`和编程模式更加容易。因此,一组相关的类有相似的`API`有时候比每个类都有各自的一套`API`更好。 -一般来说,在`Qt`中,如果没有足够的理由要使用继承,我们更倾向于用静态多态。这样可以减少`Qt`公有类的个数,也使刚学习`Qt`的用户在翻看文档时更有方向感。 +一般来说,在`Qt`中,如果没有足够的理由要使用继承,我们更倾向于用静态多态。这样可以减少`Qt` public 类的个数,也使刚学习`Qt`的用户在翻看文档时更有方向感。 ## 2.1 好的案例 @@ -156,7 +158,7 @@ regExp.setPattern("."); regExp.setPatternSyntax(Qt::WildcardSyntax); ``` -为实现这种类型的`API`,需要借助底层的对象的懒创建。例如,对于`QRegExp`的例子,在不知道模式语法(`pattern syntax`)的情况下,在`setPattern()`中编译`"."`就为时过早了。 +为实现这种类型的`API`,需要借助底层的对象的懒创建。例如,对于`QRegExp`的例子,在不知道模式语法(`pattern syntax`)的情况下,在`setPattern()`中去解释`"."`就为时过早了。 属性之间常常有关联的;在这种情况下,我们必须小心处理。思考下面的问题:当前的风格(`style`)提供了『默认的图标尺寸』属性 vs. `QToolButton`的『`iconSize`』属性: @@ -178,7 +180,7 @@ toolButton->iconSize(); // returns (52, 52) 在某些情况下,`getter`方法返回的结果与所设置的值不同。例如,虽然调用了`widget->setEnabled(true)`,但如果它的父`widget`处于`disabled`状态,那么`widget->isEnabled()`仍然返回的是`false`。这样是OK的,因为一般来说就是我们想要的检查结果(父`widget`处于`disabled`状态,里面的子`widget`也应该变为灰的不响应用户操作,就好像子`widget`自身处于`disabled`状态一样;与此同时,因为子`widget`记得在自己的内心深处是`enabled`状态的,只是一直等待着它的父`widget`变为`enabled`)。当然诸如这些都必须在文档中妥善地说明清楚。 -# 4. `C++`细节 +# 4. `C++`相关 ## 4.1 值 vs. 对象 @@ -198,15 +200,26 @@ color.getHsv(&h, &s, &v); color.getHsv(h, s, v); ``` -只有第一行代码清楚表达出`h`、`s`、`v`参数在函数调用中非常可能被修改。 +只有第一行代码清楚表达出`h`、`s`、`v`参数在函数调用中非常有可能会被修改。 -### 4.1.2 按常量引用传递 vs. 按值传递 +这也就是说,编译器并不喜欢“出参”,所你应该在新的API中避免使用“出参”,而是返回一个结构体,如下所示: -如果类型大于16字节,按常量引用传递。 +```cpp +struct Hsv { int hue, saturation, value }; +Hsv getHsv() const; +``` -如果类型有重型的(`non-trivial`)拷贝构造函数(`copy-constructor`)或是重型的析构函数(`destructor`),按常量引用传递以避免执行这些函数。 +> 【译注】:函数的“入参”和“出参”的混用会导致 API 接口语义的混乱,所以,使用指针,在调用的时候,实参需要加上“&”,这样在代码阅读的时候,可以看到是一个实参,有利有阅读。(但是这样做,在函数内就需要判断指针是否为空的情况,因为引用是不需要判断的,所以,这是一种 trade-off) +> +> 而如果这样的参与过多的话,最好使用一个结构体来把数据打包,一方面,为一组返回值取个名字,另一方面,这样有利用接口的简单。 -对于其它的类型通常应该按值传递。 +### 4.1.2 按常量引用传参 vs. 按值传参 + +如果类型大于16字节,按常量引用传参。 + +如果类型有重型的(`non-trivial`)拷贝构造函数(`copy-constructor`)或是重型的析构函数(`destructor`),按常量引用传参以避免执行这些函数。 + +对于其它的类型通常应该按值传参。 示例: @@ -222,9 +235,11 @@ void setAlarm(const QSharedPointer &alarm); // are good examples of other classes you should pass by value. ``` +>【译注】:这是传引用和传值的差别了,因为传值会有对像拷贝,传引用则不会。所以,如果对像的构造比较重的话(换句话说,就是对像里的成员变量需要的内存比较大),这就会影响很多性能。所以,为了提高性能,最好是传引用。但是如果传入引用的话,会导致这个对象可能会被改变。所以传入 const reference。 + ## 4.2 虚函数 -在`C++`中,当类的成员函数声明为`virtual`,主要是为了通过在子类重载此函数能够定制函数的行为。将函数声明为`virtual`的目的是为了让对这个函数已有的调用变成执行你的代码路径。对于没有在类外部调用的函数,是否将其声明为`virtual`你应该多加小心。 +在`C++`中,当类的成员函数声明为`virtual`,主要是为了通过在子类重载此函数能够定制函数的行为。将函数声明为`virtual`的目的是为了让对这个函数已有的调用变成执行实际实例的代码路径。对于没有在类外部调用的函数,是否将其声明为`virtual`你应该多加小心。 ```cpp // QTextEdit in Qt 3: member functions that have no reason for being virtual @@ -241,30 +256,32 @@ virtual void setCurrentFont( const QFont &f ); virtual void setOverwriteMode( bool b ) { overWrite = b; } ``` -`QTextEdit`从`Qt 3`移植到`Qt 4`的时候,几乎所有的虚函数都被移除了。有趣的是(但在预料之中),并没有人对此有大的抱怨,为什么?因为`Qt 3`没用到`QTextEdit`的多态行为 —— 只有你会;简单得说,没有理由去继承`QTextEdit`并重新实现这些函数,除非你自己调用了这些方法。如果在`Qt`在外部你的应用程序你需要多态,你可以自己添加多态。 +`QTextEdit`从`Qt 3`移植到`Qt 4`的时候,几乎所有的虚函数都被移除了。有趣的是(但在预料之中),并没有人对此有大的抱怨,为什么?因为`Qt 3`没用到`QTextEdit`的多态行为 —— 只有你会;简单地说,没有理由去继承`QTextEdit`并重新实现这些函数,除非你自己调用了这些方法。如果在`Qt`在外部你的应用程序你需要多态,你可以自己添加多态。 + +> 【译注】:“多态”的目的只不过是为了实践——“依赖于接口而不是实现”,也就是说,接口是代码抽像的一个非常重要的方式(在Java/Go中都有专的接口声明的语法)。所以,如果没有接口抽像,使用“多态”的意义也就不大了,因为也就没有必要使用“虚函数”了。 ### 4.2.1 避免虚函数 在`Qt`中,我们有很多理由尽量减少虚函数的数量。每一次对虚函数的调用会在函数调用链路中插入一个未掌控的节点(某种程度上使结果更无法预测),使得`bug`修复变得更复杂。用户在重新实现的虚函数中可以做很多疯狂的事: - 发送事件 -- 送信号 +- 发送信号 - 重新进入事件循环(例如,通过打开一个模态文件对话框) - 删除对象(即触发『`delete this`』) 还有其他很多原因要避免过度使用虚函数: -- 添加、移动或是删除虚函数都带来二进制兼容问题(`binary binary/BC`) +- 添加、移动或是删除虚函数都带来二进制兼容问题(`binary compatibility/BC`) - 重载虚函数并不容易 - 编译器几乎不能优化或内联(`inline`)对虚函数的调用 - 虚函数调用需要查找虚函数表(`v-table`),这比普通函数调用慢了2到3倍 -- 虚函数使得类很难按值复制(尽管可能,但是非常混乱并且不建议这样做) +- 虚函数使得类很难按值复制(尽管可也按值复制,但是非常混乱并且不建议这样做) 经验告诉我们,没有虚函数的类一般`bug`更少、维护成本也更低。 -一般的经验法则是,除非我们以这个类作为工具包或是作为这个类的主要用户来调用函数,否则这个函数九成不应该设计成虚函数。 +一般的经验法则是,除非我们以这个类作为工具集提供而且有很多用户来调用某个类的虚函数,否则这个函数九成不应该设计成虚函数。 -【TODO:工具包这句理解不了翻译得不清!】 +>【译注】:1)使用虚函数时,你需要对编译器的内部行为非常清楚,否则,你会在使用虚函数时,觉得有好些“古怪”的问题发生。2)在C++中,会有一个基础类,这个基础类中已经实现好了很多功能,然后把其中的一些函数放给子类去修改和实现。这种方法在父类和子类都是一组开发人员维护时没有什么问题,但是如果这是两组开发人员,这就会带来很多问题了,就像Qt这样,子类完全无法控制,全世界的开发人员想干什么就干什么。所以,子类的代码和父类的代码在兼容上就会出现很多很多问题。所以,还是上面所说,其实,虚函数应该声明在接口的语义里(这就是设计模式的两个宗旨——依赖于接口,而不是实现;钟爱于组合,而不是继承) ### 4.2.2 虚函数 vs. 复制 @@ -288,7 +305,7 @@ public: }; ``` -如果继承`CopyClass`这个类,预料之外的事就已经在代码时酝酿了。一般情况下,如果没有虚成员函数和虚析构函数,就不能有依赖多态的子类。然而,如果存在虚成员函数和虚析构函数,这突然变成了要有子类去继承的理由,而且开始变得复杂了。**_起初认为只要简单声明上虚操作符重载函数(`virtual operators`)。_** 但其实是走上了一条混乱和毁灭之路(看不明白?读作写出的是『不可读代码』)。看看下面的这个例子: +如果继承`CopyClass`这个类,预料之外的事就已经在代码时酝酿了。一般情况下,如果没有虚成员函数和虚析构函数,就不能创建出可以多态的子类。然而,如果存在虚成员函数和虚析构函数,这突然变成了要有子类去继承的理由,而且开始变得复杂了。**_起初认为只要简单声明上虚操作符重载函数(`virtual operators`)。_** 但其实是走上了一条混乱和毁灭之路(破坏了代码的可读性)。看看下面的这个例子: ```cpp class OtherClass { @@ -297,13 +314,15 @@ public: }; ``` -(未完等续) +(这部份还未完成) + +>【译注】:因为原文上说,这部份并没有完成,所以,我也没有搞懂原文具体也是想表达什么。不过,就标题而言,原文是想说,在多态的情况下复制对象所带来的问题?? ## 4.3 关于`const` **_`C++`的关键词`const`表明了内容不会改变或是没有副作用。可以应用于简单的值、指针及指针所指的内容,也可以作为一个特别的属性应用于类的成员函数上,表示成员函数不能修改对象的状态。_** -然而,`const`本身并没有提供太大的价值 —— 很多编程语言甚至没有类似`const`的关键词,但是却并没有因此产生问题。实际上,如果你不用函数重载,并在`C++`源代码用搜索和替换来删除所有的`const`,几乎总能编译通过并且正常运行。尽量让使用的`const`保持实用有效,这点很重要。 +然而,`const`本身并没有提供太大的价值 —— 很多编程语言甚至没有类似`const`的关键词,但是却并没有因此产生问题。实际上,如果你不用函数重载,并在`C++`源代码用搜索并删除所有的`const`,几乎总能编译通过并且正常运行。尽量让使用的`const`保持实用有效,这点很重要。 让我们看一下在`Qt`的`API`设计中与`const`相关的场景。 @@ -348,9 +367,7 @@ QPointF mapFromItem (const QGraphicsItem *item, const QPointF &point) const; 当在类类型(`class type`)右值上添加`const`关键字,则禁止访问非`const`成员函数以及对成员的直接操作。 -不加`const`则没有以上的限制,但几乎没有必要加上`const`,因为右值对象生存时间(`life time`)的结束一般在`C++`全清理(`full-removed`)的时候(通俗的说,下一个分号地方),而对右值对象的修改随着右值对象的生存时间就一起结束了。 - -【TODO:什么是full-removed ??需要调查理解!】 +不加`const`则没有以上的限制,但几乎没有必要加上`const`,因为右值对象生存时间(`life time`)的结束一般在`C++`清理的时候(通俗的说,下一个分号地方),而对右值对象的修改随着右值对象的生存时间也一起结束了(也就是本条语句的执行完成的时候)。 示例: @@ -389,6 +406,8 @@ int main() { } ``` +> 【译注】:上述的代码说明,如果返回值不是 const 的,代码可以顺利编译通过,然后并没有什么卵用,因为那个临时对像马上就被抛弃了。所以,这样的无用的代码最好还是在编译时报个错,以免当时头脑发热想错了。 + ### 4.3.3 返回值:非`const`的指针还是有`const`的指针 谈到`const`函数应该返回非`const`的指针还是`const`指针这个话题时,多数人发现在`C++`中关于『`const`正确性』(`const correctness`)在概念上产生了分歧。 _问题起源是:**`const`函数本身不能修改对象自身的状态,却可以返回成员的非`const`指针**。返回指针这个简单动作本身既不会影响整个对象的可见状态,当然也不会改变这个函数职责范围内涉及的状态。但是,这却使得程序员可以间接访问并修改对象的状态。_ @@ -427,6 +446,8 @@ foreach (const QGraphicsItem *item, scene.items()) { (以`d-point`的典型做法(`idiom`)为例,我们可以在任何时候改变`Qt`类在内存表示(`memory representation`);但却不能在不破坏二进制兼容性的情况下把改变函数的签名,返回值从`const QFoo &`变为`QFoo`。) 基于这个原因,除去对运行速度敏感(`speed is critical`)而重构不是问题的个别情形(例如,`QList::at()`),我们一般返回`QFoo`而不是`const QFoo &`。 +> 【译注】:《Effective C++》中条款23:Don't try to return a reference when you must return an object + ### 4.4.5 `const` vs. 对象的状态 `const`正确性的问题就像`C`圈子中`vi`与`emacs`讨论,因为这个话题在很多地方都存在分歧(比如基于指针的函数)。 @@ -491,6 +512,8 @@ void QGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem opti 当你找不到好的名称时,开始写文档是一种好好的寻找方式:尝试为类、方法、枚举类型、值等写文档,把写下的第一句作为启发。如果找不确切的名称,这说明这个东西不该存在。如果所有尝试都失败了,并且你认为不如发明一个新名称,你就知道`widget`,`event focus`和`buddy`是如何产生的了。 +> 【译注】:写文档是一个非常好的习惯。写文档的过程其实就是在帮你梳理你的编程思路。很多时候,写着写文档你就会发现,你要去改代码去了。除了上述的好处多,写文档还有更多的好处。比如,在写文档的过程中,你发现文字描述过于复杂了,这表明着你的代码或逻辑是复杂的,这就倒逼你去重构你的代码。所以——**写文档其实就是写代码**。 + ## 6.2 类的命名 用把类的名称分组的方式替换为每个类单独命名的方法。例如,所有`Qt 4`的了解模型(`model-aware`)的视图(`view`)类后缀都是`View`(`QListView`、`QTableView`、`QTreeView`),相应的基于`item`的类后缀是`Widget`(`QListWidget`、`QTableWidget`、`QTreeWidget`)。 @@ -516,10 +539,9 @@ str.indexOf("$(QTDIR)", Qt::Insensitive); ```cpp namespace Qt { -enum Corner { TopLeftCorner, BottomRightCorner, … }; -enum CaseSensitivity { CaseInsensitive, -CaseSensitive }; -… + enum Corner { TopLeftCorner, BottomRightCorner, … }; + enum CaseSensitivity { CaseInsensitive, CaseSensitive }; + … }; tabWidget->setCornerWidget(widget, Qt::TopLeftCorner); @@ -594,6 +616,8 @@ slider->setValue(13); slider->setObjectName("volume"); ``` +>【译注】:在有 IDE 的自动提示的支持下,后者写起来非常方便,而前者还需要看相应的文档。 + ## 7.2 `Boolean`参数的陷阱 `Boolean`类型的参数总是带来无法阅读的代码。给现有的函数增加一个`bool`型的参数几乎永远是一种错误的行为。仍以`Qt`为例,`repaint()`有一个`bool`类型的可选参数用于指定背景是否被擦除。可以写出这样的代码: