注: 本文摘自 Swift API设计指南
- 通俗易懂的API是设计者最重要的目标。实体、变量、函数等都具有一次申明、重复使用的性质,所以一个好的API设计,应该能够使用少量的解读和示例就可以清晰的表达它的语意和用途。
- 应该将代码的设计重点放在如何使其逻辑更加清晰之上,而不是追求简短。Swift确实可以写出非常简短的代码,但是这不应该是设计者的首要目的。毕竟在代码的简短之道上,Swift的语言特质其实已经帮我们做了太多了。
- 每一个API都应该注释必要的文档。 这在短期内可能效果不大,但是长远影响深远。(如果在简单的描述API功能的方面遇到困难,可能是因为设计了错误的API。)
注释文档示例:
-
使用 Swift 专门的注释方式。下面这张图表示注释的内容在PlayGround上的对应关系。
在首行描述API的总体信息。 如:
/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection
-
或者,使用分段式的注释,每一种类型作为一个段落,段落之间以空行分割。如:
/// Writes the textual representation of each ← Summary
/// element of `items` to the standard output.
/// ← Blank line
/// The textual representation for each item `x` ← Additional discussion
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed ⎫
/// between items. ⎟
/// - Parameter terminator: text to be printed ⎬ Parameters section
/// at the end. ⎟
/// ⎭
/// - Note: To print without a trailing ⎫
/// newline, pass `terminator: ""` ⎟
/// ⎬ Symbol commands
/// - SeeAlso: `CustomDebugStringConvertible`, ⎟
/// `CustomStringConvertible`, `debugPrint`. ⎭
public func print(
_ items: Any..., separator: String = " ", terminator: String = "\n")
另: 使用系统识别的项目符号进行注释,使用规范的用词对应符合的内容, 使读者更容易明白API的细节含义。附表。
二、API命名规则
1. 促进通俗使用。
-
API中的名字中,应该包含所有的必要的名词,并尽量消除歧义。
一个不好的例子:
employees.remove(x) // unclear: are we removing x?
改正:
extension List { public mutating func remove(at position: Index) -> Element }
employees.remove(at: x)
-
消除不必要的话,API中每个单词都应该有其显著的意思。
有时候需要陈述一些东西使得API的语意,但是不要重复,下面这个就显得很啰嗦了
public mutation func removeElement(_ member:Element) - >元素? 包含两次元素的描述了
allViews.removeElement(cancelButton)
可以改正:
public mutating func remove(_ member: Element) -> Element?
allViews.remove(cancelButton) // clearer
错误的示例 :
ClassProductionLine {
func restock(from widgetFactory: WidgetFactory) widgetFactory -- 类型
}
改正:
ClassProductionLine { func restock(from supplier: WidgetFactory) supplier -- 角色
}
Swift中并没有包含所有你要的类型,比如 keyPath, 尽管它被表示路径,但是实际上它的真实类型是String,而我们所理解的keyPath"类型",就是弱类型。对于弱类型,为了恢复其清晰度,在每个 弱类型参数之前加上描述其作用的名词。
错误的示例:
func add(_ observer: NSObject, for keyPath: String) 路径 -- 弱类型
grid.add(self, for: graphics) // vague
改善:
func addObserver(_ observer: NSObject, forKeyPath path: String) forKeyPath:用于描述参数的作用
grid.addObserver(self, forKeyPath: graphics) // clear
2.争取使得使用者可以流利的使用
这种方式能够让使用者看上一遍就基本上明白API的作用了。来看一个比较不错的例子:
x.insert(y, at: z) “x, insert y at z”
x.subViews(havingColor: y) “x's subviews having color y”
x.capitalizingNouns() “x, capitalizing nouns”
x.insert(y, position: z)
x.subViews(color: y)
x.nounCapitalize()
-
初始化程序和工厂方法调用应该形成一个不包含第一个参数的短语
不好的方式举例:
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
改正 :
let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
1. 没有具体作用的函数和方法应该看作名词短语,例如 x.distance(to:y),i.successor()。
2. 具有具体作用的函数和方法应该将其视为必需的动词短语,例如print(x),x.sort(),x.append(y)。
3. 根据是否会对调用者发生突变进行适当的修改名字。 突变方法通常应当具有类似语义的非显式变体,它的结果会返回一个新值,而不是就地更新实例。
(1)突变和非突变函数命名的区别。 (当用动词描述函数时,将函数命名为动词,并应用“ed”或“ing”后缀来命名非突变的函数。下表1示例)
(2)突变和非突变函数命名的区别。 (当使用名词描述时,将函数命名为名词,并应用“form”前缀命名其突变函数。下表2示例)
Mutating 突变 |
Nonmutating 非突变 |
x.sort() |
z = x.sorted() |
x.append(y) |
z = x.appending(y)
|
表1
Nonmutating 非突变 |
Mutating 突变
|
x = y.union(z) |
y.formUnion(z) |
j = c.successor(i) |
c.formSuccessor(&i) |
表2
4.对于返回为bool值的函数,应当使用肯定的语气进行断言。(描述的不是很到位,总之不能产生歧义。)
e.g. x.isEmpty, line1.intersects(line2) 只读的情况下。
5. 描述协议的时候,应该直接使用名词来表示。
6. 描述 应使用、可以使用的的时候,使用后缀追加命名方式。
e.g. Equatable,ProgressReporting
7. 其他类型,属性,变量和常量的名称应以名义显示。
3.更好的使用术语
术语,是指在特定的领域中,用一些简短的、非常用的词语描述这个领域中某些东西。比如我们在计算机中经常使用的CPU,它代表中央处理器,显然这是个人尽皆知的名词,人们一眼就能看懂它代表的是什么,这就是一个术语。
在程序中,能够使用编程中的术语的时候我们尽可能的使用它,而不是应该自己去编写一段其他的名词代替。当然,不仅仅是在计算机领域,在其他领域的专业术语也可以使用,最好的前提是,我们正在编写与这个领域相关的程序。比如在编写图片处理的程序的时候,我们不可避免的会使用到位图这个概念,于是我们在程序中将有 ‘bitMap’这个术语了。
大部分术语源自于一个比较长的单词的首字母缩写,但这不代表我们在开发中的描述也可以这么做,术语有它众所周知的语意,相反,而随意的缩写并不具备这个功能。所以我们在考虑缩短编写内容的时候,应该还要考虑到其他人是否能明白你要表达的意思。 术语就是典型的例子。
三、符合常识的设计准则
1.遵循公众习惯
这种情况只在某些特定的范围之下。
1. 当函数是一个无约束的泛型时。 我们不知道它的具体用途,只知道它的操作过程。比如:
2.当函数的名字已经是公众的常识的一部分的时候。 比如:
print(x) -- 所有人都知道这是一个打印的函数
3.使用了特定领域的类似于公式的时候。为了让特定领域的编程更加符合使用者的常识,我们不应该更改它的结构。 比如:
sin(x) -- 如果使用 sin(value:x),很多人会觉得多此一举了。
如果在API中需要用到 NBA 这个名字的时候,那么应该直接使用NBA,不管这个单词出现在命名的那一部分。 而不是 Nba、nba之类。 还有一些例子:
var utf8Bytes: [UTF8.CodeUnit] var isRepresentableAsASCII = true
var userSMTPServer: SecureSMTPServer
除此之外,其余的函数书写应该按照驼峰式书写 -- 非首个单词的首字母大写。
1. 例如,swift鼓励以下方法,因为方法基本上是相同的:
extension Shape {
/// Returns `true` iff `other` is within the area of `self`.
func contains(_ other: Point) -> Bool { ... }
/// Returns `true` iff `other` is entirely within the area of `self`.
func contains(_ other: Shape) -> Bool { ... }
/// Returns `true` iff `other` is within the area of `self`.
func contains(_ other: LineSegment) -> Bool { ... }
}
当然,他们其实可以使用一个函数来替代: (这是一个高阶函数)
extension Collection where Element : Equatable {
/// Returns `true` iff `self` contains an element equal to
/// `sought`.
func contains(_ sought: Element) -> Bool { ... }
}
2.如果使用过头的话也是不对的,比如下面这种方式:
extension Database {
/// Rebuilds the database's search index
func index() { ... }
/// Returns the `n`th row in the given table.
func index(_ n: Int, inTable: TableID) -> TableRow { ... }
}
很明显,这两个函数具有不同的意思。他们应该使用不同的名字。下面这种也是不行的。
extension Box {
/// Returns the `Int` stored in `self`, if any, and
/// `nil` otherwise.
func value() -> Int? { ... }
/// Returns the `String` stored in `self`, if any, and
/// `nil` otherwise.
func value() -> String? { ... }
}
错误的原因是: 不应该在返回类型上重载, 编译器会不知道如何选择类型推断的。
2.参数
尽管在文档中,参数没有任何实际的作用,但是它可以更好的帮助其他人理解API的含义。
/// Return an `Array` containing the elements of `self`
/// that satisfy `predicate`.
func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
/// Replace the given `subRange` of elements with `newElements`.
mutating func replaceRange(_ subRange: Range, with newElements: [E])
下面这个就显得让人费解。
/// Return an `Array` containing the elements of `self`
/// that satisfy `includedInResult`.
func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]
/// Replace the range of elements indicated by `r` with
/// the contents of `with`.
mutating func replaceRange(_ r: Range, with: [E])
有时候我们可能会使用到一个函数,其中的参数不是每次都需要的,也许有些开发者会这样:
extension String {
/// ...description 1...
public func compare(_ other: String) -> Ordering
/// ...description 2...
public func compare(_ other: String, options: CompareOptions) -> Ordering
/// ...description 3...
public func compare(
_ other: String, options: CompareOptions, range: Range) -> Ordering
/// ...description 4...
public func compare(
_ other: String, options: StringCompareOptions,
range: Range, locale: Locale) -> Ordering
}
似乎是把所有的函数都包含进来了,但是。。。 太繁琐了! 可以进行改进嘛,如果不需要的参数,我们可以使用nil或者空的对象代替。像这样:
let order = lastName.compare(
royalFamilyName, options: [], range: nil, locale: nil)
确实有点效果,至少这里看起来简单多了。 但是一想到有的参数90%的时间都没有用上的时候,是不是觉得很尴尬? 试试下面的方法:
extension String {
/// ...description...
public func compare(
_ other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
) -> Ordering
}
let order = lastName.compare(royalFamilyName)
这样是不是舒服多了。 采用默认参数的方式,让参数的内容变得更加明了。
3.参数标签(参数名)
e.g. min(number1, number2), zip(sequence1, sequence2)
-
对象执行类型转换并保存的函数中,应该省略第一个参数的标签
-
当第一个参数形成介词短语的一部分时,给它一个参数标签
a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
-
如果一个函数的命名中已经包含了第一个参数的语意,则第一个参数的标签可以省略
同时这意味着,如果第一个参数没有被命名语法包含,那么他将必须要有一个标签,就像这样:
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
错误的示范:
view.dismiss(false) Don't dismiss? Dismiss a Bool? 不删除 还是删除一个BOOL
words.split(12) Split the number 12? 拆分 12?
四 、特别说明
-
闭包中的标签应当同样作为正常的参数标签,写入文档中注释 。
-
具有重载参数的函数中,应当给予适当的标签。用于避免重载引起的歧义。
正确的例子:
struct Array {
/// Inserts `newElement` at `self.endIndex`.
public mutating func append(_ newElement: Element)
/// Inserts the contents of `newElements`, in order, at
/// `self.endIndex`.
public mutating func append(contentsOf newElements: S)
where S.Generator.Element == Element
}
不好的例子:
struct Array {
/// Inserts `newElement` at `self.endIndex`.
public mutating func append(_ newElement: Element)
/// Inserts the contents of `newElements`, in order, at
/// `self.endIndex`.
public mutating func append(_ newElements: S)
where S.Generator.Element == Element }
最后,请注意新名称如何更好地符合文档注释。 在这种情况下,撰写文档注释的行为实际上是将问题转化成了API作者的关注。
|
请发表评论