Swift API设计原则
注: 本文摘自 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的细节含义。附表。
Attention 核心、划重点 | Author 注明作者 | Authors 一群作者(开发者) | Bug bug标记 |
Complexity 复杂度著名 ,比如循环复杂度 | Copyright 著作全申明 | Date 日期 | Experiment 试验? |
Important 强调 | Invariant 常量 | Note 注意事项 | Parameter 参数 |
Parameters 多参数 | Postcondition 后置条件 | Precondition 前提条件 | Remark 标注评论 |
Requires 必须的 | Returns 返回 | SeeAlso 参考-参见 | Since 自。。开始 |
Throws 抛出、反馈 | Todo 接下来做 | Version 版本 | Warning 警告 |
二、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的读取趋近于英文中的语法使用
这种方式能够让使用者看上一遍就基本上明白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()
-
使用‘ make ’作为创建对象的前缀
eg. x.makeIterator()
-
初始化程序和工厂方法调用应该形成一个不包含第一个参数的短语
不好的方式举例:
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. 描述协议的时候,应该直接使用名词来表示。
e.g. Collection
6. 描述 应使用、可以使用的的时候,使用后缀追加命名方式。
e.g. Equatable,ProgressReporting
7. 其他类型,属性,变量和常量的名称应以名义显示。
3.更好的使用术语
术语,是指在特定的领域中,用一些简短的、非常用的词语描述这个领域中某些东西。比如我们在计算机中经常使用的CPU,它代表中央处理器,显然这是个人尽皆知的名词,人们一眼就能看懂它代表的是什么,这就是一个术语。
在程序中,能够使用编程中的术语的时候我们尽可能的使用它,而不是应该自己去编写一段其他的名词代替。当然,不仅仅是在计算机领域,在其他领域的专业术语也可以使用,最好的前提是,我们正在编写与这个领域相关的程序。比如在编写图片处理的程序的时候,我们不可避免的会使用到位图这个概念,于是我们在程序中将有 ‘bitMap’这个术语了。
大部分术语源自于一个比较长的单词的首字母缩写,但这不代表我们在开发中的描述也可以这么做,术语有它众所周知的语意,相反,而随意的缩写并不具备这个功能。所以我们在考虑缩短编写内容的时候,应该还要考虑到其他人是否能明白你要表达的意思。 术语就是典型的例子。
三、符合常识的设计准则
1.遵循公众习惯
-
使用无参数描述的函数
这种情况只在某些特定的范围之下。
1. 当函数是一个无约束的泛型时。 我们不知道它的具体用途,只知道它的操作过程。比如:
min(x, y, z)
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)
-
对象执行类型转换并保存的函数中,应该省略第一个参数的标签
e.g. Int64(someUInt32)
-
当第一个参数形成介词短语的一部分时,给它一个参数标签
a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
-
如果一个函数的命名中已经包含了第一个参数的语意,则第一个参数的标签可以省略
e.g. x.addSubview(y)
同时这意味着,如果第一个参数没有被命名语法包含,那么他将必须要有一个标签,就像这样:
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作者的关注。