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作者的关注。

 

 

 

 

 

 

 

posted @ 2017-09-19 17:35  BennyLoo  阅读(1197)  评论(0编辑  收藏  举报