diff --git a/api-design-principles-from-qt/README.md b/api-design-principles-from-qt/README.md index 416f373..7e6c187 100644 --- a/api-design-principles-from-qt/README.md +++ b/api-design-principles-from-qt/README.md @@ -5,7 +5,7 @@ `Qt`的设计水准在业界很有口碑,一致、易于掌握和强大的`API`是`Qt`最著名的优点之一。此文既是`Qt`官网上的`API`设计指导准则,也是`Qt`在`API`设计上的实践总结。虽然`Qt`用的是`C++`,但其中设计原则和思考是具有普适性的(如果你对`C++`还不精通,可以忽略与`C++`强相关或是过于细节的部分,仍然可以学习或梳理关于`API`设计最有价值的内容)。整个篇幅中有很多示例,是关于`API`设计一篇难得的好文章。 -需要注意的是,这篇 Wiki 有一些东西并不完整,所以,可能会有一些阅读上的问题。我们对此做了一些相关的注释。 +需要注意的是,这篇`Wiki`有一些内容并不完整,所以,可能会有一些阅读上的问题,我们对此做了一些相关的注释。 # `API`设计原则 @@ -54,10 +54,10 @@ - [6.2 类的命名](#62-%E7%B1%BB%E7%9A%84%E5%91%BD%E5%90%8D) - [6.3 枚举类型及其值的命名](#63-%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B%E5%8F%8A%E5%85%B6%E5%80%BC%E7%9A%84%E5%91%BD%E5%90%8D) - [6.4 函数和参数的命名](#64-%E5%87%BD%E6%95%B0%E5%92%8C%E5%8F%82%E6%95%B0%E7%9A%84%E5%91%BD%E5%90%8D) - - [6.5 `Boolean`类型的`getter`与`setter`方法的命名](#65-boolean%E7%B1%BB%E5%9E%8B%E7%9A%84getter%E4%B8%8Esetter%E6%96%B9%E6%B3%95%E7%9A%84%E5%91%BD%E5%90%8D) + - [6.5 布尔类型的`getter`与`setter`方法的命名](#65-%E5%B8%83%E5%B0%94%E7%B1%BB%E5%9E%8B%E7%9A%84getter%E4%B8%8Esetter%E6%96%B9%E6%B3%95%E7%9A%84%E5%91%BD%E5%90%8D) - [7. 避免常见陷阱](#7-%E9%81%BF%E5%85%8D%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1) - [7.1 简化的陷阱](#71-%E7%AE%80%E5%8C%96%E7%9A%84%E9%99%B7%E9%98%B1) - - [7.2 `Boolean`参数的陷阱](#72-boolean%E5%8F%82%E6%95%B0%E7%9A%84%E9%99%B7%E9%98%B1) + - [7.2 布尔参数的陷阱](#72-%E5%B8%83%E5%B0%94%E5%8F%82%E6%95%B0%E7%9A%84%E9%99%B7%E9%98%B1) - [8. 案例研究](#8-%E6%A1%88%E4%BE%8B%E7%A0%94%E7%A9%B6) - [8.1 `QProgressBar`](#81-qprogressbar) - [8.2 `QAbstractPrintDialog` & `QAbstractPageSizeDialog`](#82-qabstractprintdialog--qabstractpagesizedialog) @@ -85,11 +85,11 @@ ## 1.3 语义清晰简单 -就像其他的设计一样,我们应该遵守最少意外原则(`the principle of least surprise`)。好的API应该可以让常见的事完成的更简单,并有可以完成不常见的事的可能性,但是却不会关注于那些不常见的事。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在`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 易于记忆 @@ -141,7 +141,7 @@ timer.setInterval(1000); timer.start(); ``` ->【译注】:正交特性:改变某个特性而不会影响到其他的特性。《程序员修炼之道》中讲了一个关于正交性的直升飞机坠毁的例子。 +>【译注】:正交性是指改变某个特性而不会影响到其他的特性。[《程序员修炼之道》](https://book.douban.com/subject/5387402/)中讲了关于正交性的一个直升飞机坠毁的例子,讲得深入浅出很有画面感。 为了方便,也写成: @@ -209,8 +209,8 @@ struct Hsv { int hue, saturation, value }; Hsv getHsv() const; ``` -> 【译注】:函数的“入参”和“出参”的混用会导致 API 接口语义的混乱,所以,使用指针,在调用的时候,实参需要加上“&”,这样在代码阅读的时候,可以看到是一个实参,有利有阅读。(但是这样做,在函数内就需要判断指针是否为空的情况,因为引用是不需要判断的,所以,这是一种 trade-off) -> +> 【译注】:函数的『入参』和『出参』的混用会导致`API`接口语义的混乱,所以,使用指针,在调用的时候,实参需要加上`&`,这样在代码阅读的时候,可以看到是一个实参,有利有阅读。(但是这样做,在函数内就需要判断指针是否为空的情况,因为引用是不需要判断的,所以,这是一种 trade-off) +> > 而如果这样的参与过多的话,最好使用一个结构体来把数据打包,一方面,为一组返回值取个名字,另一方面,这样有利用接口的简单。 ### 4.1.2 按常量引用传参 vs. 按值传参 @@ -258,7 +258,7 @@ virtual void setOverwriteMode( bool b ) { overWrite = b; } `QTextEdit`从`Qt 3`移植到`Qt 4`的时候,几乎所有的虚函数都被移除了。有趣的是(但在预料之中),并没有人对此有大的抱怨,为什么?因为`Qt 3`没用到`QTextEdit`的多态行为 —— 只有你会;简单地说,没有理由去继承`QTextEdit`并重新实现这些函数,除非你自己调用了这些方法。如果在`Qt`在外部你的应用程序你需要多态,你可以自己添加多态。 -> 【译注】:『多态』的目的只不过是为了实践 —— 『依赖于接口而不是实现』,也就是说,接口是代码抽像的一个非常重要的方式(在`Java/Go`中都有专的接口声明的语法)。所以,如果没有接口抽像,使用『多态』的意义也就不大了,因为也就没有必要使用『虚函数』了。 +> 【译注】:『多态』的目的只不过是为了实践 —— 『依赖于接口而不是实现』,也就是说,接口是代码抽像的一个非常重要的方式(在`Java/Go`中都有专门的接口声明的语法)。所以,如果没有接口抽像,使用『多态』的意义也就不大了,因为也就没有必要使用『虚函数』了。 ### 4.2.1 避免虚函数 @@ -281,7 +281,12 @@ virtual void setOverwriteMode( bool b ) { overWrite = b; } 一般的经验法则是,除非我们以这个类作为工具集提供而且有很多用户来调用某个类的虚函数,否则这个函数九成不应该设计成虚函数。 ->【译注】:1) 使用虚函数时,你需要对编译器的内部行为非常清楚,否则,你会在使用虚函数时,觉得有好些『古怪』的问题发生。2) 在`C++`中,会有一个基础类,这个基础类中已经实现好了很多功能,然后把其中的一些函数放给子类去修改和实现。这种方法在父类和子类都是一组开发人员维护时没有什么问题,但是如果这是两组开发人员,这就会带来很多问题了,就像`Qt`这样,子类完全无法控制,全世界的开发人员想干什么就干什么。所以,子类的代码和父类的代码在兼容上就会出现很多很多问题。所以,还是上面所说,其实,虚函数应该声明在接口的语义里(这就是设计模式的两个宗旨——依赖于接口,而不是实现;钟爱于组合,而不是继承) +>【译注】: +> +> 1. 使用虚函数时,你需要对编译器的内部行为非常清楚,否则,你会在使用虚函数时,觉得有好些『古怪』的问题发生。 +> 2. 在`C++`中,会有一个基础类,这个基础类中已经实现好了很多功能,然后把其中的一些函数放给子类去修改和实现。这种方法在父类和子类都是一组开发人员维护时没有什么问题,但是如果这是两组开发人员,这就会带来很多问题了,就像`Qt`这样,子类完全无法控制,全世界的开发人员想干什么就干什么。所以,子类的代码和父类的代码在兼容上就会出现很多很多问题。 +> +> 所以,还是上面所说,其实,虚函数应该声明在接口的语义里(这就是设计模式的两个宗旨 —— 依赖于接口,而不是实现;钟爱于组合,而不是继承)。 ### 4.2.2 虚函数 vs. 复制 @@ -446,11 +451,11 @@ 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. +> 【译注】:《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`讨论,因为这个话题在很多地方都存在分歧(比如基于指针的函数)。 +`const`正确性的问题就像`C`圈子中`vi`与`emacs`的讨论,因为这个话题在很多地方都存在分歧(比如基于指针的函数)。 但通用准则是`const`函数不能改变类的可见状态。『状态』的意思是『自身以及涉及的职责』。这并不是指非`const`函数能够改变自身的私有成员,也不是指`const`函数改变不了。而是指函数是活跃的并存在可见的副作用(`visible side effects`)。`const`函数一般没有任何可见的副作用,比如: @@ -497,11 +502,11 @@ void QGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem opti ## 6.1 通用的命名规则 -有几个规则对于所有类型的命名都等同适用。第一个,之前已经提到过,不要使用缩写。即使是明显的缩写,比如把`previous`缩写成`prev`,从长远来是回报是负的,因为用户必须要记住缩写词的含义。 +有几个规则对于所有类型的命名都等同适用。第一个,之前已经提到过,不要使用缩写。即使是明显的缩写,比如把`previous`缩写成`prev`,从长远来看是回报是负的,因为用户必须要记住缩写词的含义。 如果`API`本身没有一致性,之后事情自然就会越来越糟;例如,`Qt 3` 中同时存在`activatePreviousWindow()`与`fetchPrev()`。恪守『不缩写』规则更容易地创建一致性的`API`。 -另一个时重要但更微妙的准则是在设计类时应该保持子类名称空间的干净。在`Qt 3`中,此项准则没有被一直追随。以`QToolButton`为例对此进行说明。如果调用`QToolButton`的 `name()`、`caption()`、`text()`或者`textLabel()`,你觉得会返回什么?用`Qt`设计器在`QToolButton`上自己先试试吧: +另一个时重要但更微妙的准则是在设计类时应该保持子类名称空间的干净。在`Qt 3`中,此项准则并没有一直遵循。以`QToolButton`为例对此进行说明。如果调用`QToolButton`的 `name()`、`caption()`、`text()`或者`textLabel()`,你觉得会返回什么?用`Qt`设计器在`QToolButton`上自己先试试吧: - `name`属性是继承自`QObject`,返回内部的对象名称,用于调试和测试。  - `caption`属性继承自`QWidget`,返回窗口标题,对`QToolButton`来说毫无意义,因为它在创建的时候parent就存在了。 @@ -539,9 +544,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); @@ -570,7 +575,7 @@ typedef QFlags Alignment; 虽然参数名称不会在使用`API`的代码中出现,但是它们给程序员提供了重要信息。因为现在的`IDE`都会在写代码时显示参数名称,所以应该在头文件中给参数起一个恰当的名称并在文档中使用相同的名称。 -## 6.5 `Boolean`类型的`getter`与`setter`方法的命名 +## 6.5 布尔类型的`getter`与`setter`方法的命名 为`bool`属性的`getter`和`setter`方法命名总是很痛苦。`getter`应该叫做`checked()`还是`isChecked()`?`scrollBarsEnabled()`还是`areScrollBarEnabled()`? @@ -618,9 +623,9 @@ slider->setObjectName("volume"); >【译注】:在有`IDE`的自动提示的支持下,后者写起来非常方便,而前者还需要看相应的文档。 -## 7.2 `Boolean`参数的陷阱 +## 7.2 布尔参数的陷阱 -`Boolean`类型的参数总是带来无法阅读的代码。给现有的函数增加一个`bool`型的参数几乎永远是一种错误的行为。仍以`Qt`为例,`repaint()`有一个`bool`类型的可选参数用于指定背景是否被擦除。可以写出这样的代码: +布尔类型的参数总是带来无法阅读的代码。给现有的函数增加一个`bool`型的参数几乎永远是一种错误的行为。仍以`Qt`为例,`repaint()`有一个`bool`类型的可选参数用于指定背景是否被擦除。可以写出这样的代码: ```cpp widget->repaint(false);