神刀安全网

Swift 3 SE-0023 API设计指南

本文由泊学翻译
摘自:https://github.com/Boxue/swift-api-design-guidelines/blob/master/SE-0023%20swift-api-guidelines.md

API设计原则


第一部分 基本原则

1.1 简以至用

这是最重要的设计目标。方法(method)和属性(property)只被声明一次,却会被无数次反复使用。API的简洁和清晰是要体现在它们被使用的时候。当评估一个设计的时候,仅仅去读它的声明是远远不够的,要反复在它的使用环境里推敲这个设计,并确保它足够简洁;

1.2 明确重于简短

尽管Swift代码可以很简短,但用最少的字符去编程并不是Swift的初衷。Swift中出现的简短代码只是强类型语言带来的一个副作用而已,因为可以在类型约束下推导出很多代码,因此省略了很多不必要的代码模板;

1.3 用文档的风格写注释

在每一个声明前,你应该如此。写文档时获得的灵感可以对你的设计思路有极大启发,所以,不要想着编码后回来补文档。

实际上,如果你无法三三两两就描述出API要完成的任务,你的API也许设计错了

另外,对于文档注释,还有以下补充说明:

1.3.1 使用[Swift Markdown方言]编写注释
1.3.2 在任何声明之前,(让注释)以一个摘要开始

通常,应该让读者可以根据这个摘要和声明完全理解API的功能。例如:

/// Returns a "view" of `self` containing the same elements in /// reverse order. func reversed() -> ReverseCollection
  • 专注于写摘要

这是(所有注释部分中)最重要的构成。很多出色的注释文档只有一条出色的摘要。

  • 使用一个单一的语句片段

如果可能,尽量避免在摘要中使用一个完整的句子。使用一个表意清晰的语句片段即可,并且使用点号(即 .)表示结束。

  • 采用“描述一个函数或方法会做那些操作、返回什么”这样的句式

如果函数只是简单返回内容,就忽略掉做什么的部分;如果函数返回Void
,就忽略返回值的描述,例如:

/// Inserts `newHead` at the beginning of `self`. mutating func prepend(newHead: Int)  /// Returns a `List` containing `head` followed by the elements /// of `self`. func prepending(head: Element) -> List  /// Removes and returns the first element of `self` if non-empty; /// returns `nil` otherwise. mutating func popFirst() -> Element?

当然,popFirst
方法出现了一些例外,我们使用了两句话描述了摘要。当遇到这种情况时,使用分号分隔开多个语句。

  • 描述一个下标具体访问的元素类型,例如:
/// Accesses the `index`th element. subscript(index: Int) -> Element { get set }
  • 描述一个init方法究竟创建了什么**,例如:
/// Creates an instance containing `n` repetitions of `x`. init(count n: Int, repeatedElement x: Element)
  • 对于其它的声明来说,描述它们是什么,例如:
/// A collection that supports equally efficient insertion/removal /// at any position. struct List {  /// The element at the beginning of `self`, or `nil` if self is /// empty. var first: Element?...
1.3.3 这条是可选的,(在summary之后),用一系列段落和列表项来对声明进行补充说明

段落之间用一个空行表示,并且在每一段中使用完整的语句描述。例如:

/// 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")

一些流行的开发工具,例如:Xcode,可以对文档中的下列关键字做特殊处理,并突出显示它们:
Attention | Author | Authors | Bug Complexity | Copyright | Date | Experiment Important | Invariant | Note | ParameterParameters | Postcondition | Precondition | Remark Requires | Returns | SeeAlso | Since Throws | Todo | Version | Warning


第二部分 命名规则

2.1 为了更清晰的用法而改进

2.1.1 包含所有为了避免歧义的单词

包含所有当阅读代码时会引起歧义的单词。例如:定义一个在集合中删除元素的方法:

extension List {      public mutating func remove(at position: Index) -> Element } employees.remove(at: x)

如果忽略掉函数签名中的at,remove就会被人误解为在集合中搜索x并且删除,而不是把x当成要删除元素的索引。

employees.remove(x) // unclear: are we removing x?
2.1.2 忽略不需要的单词

每一个单词都应该为它自己的表意尽职尽责。用更多单词来表明含义或避免混淆是没问题的。但是,对于那些读者已经可以顺利推断出含义的词,应该被忽略,特别是那些仅仅用来重复类型信息的词。例如:

public mutating func removeElement(member: Element) -> Element? allViews.removeElement(cancelButton)

在这个例子里,Element没有在调用时传递更多有用的信息。把API设计成这样会更好:

public mutating func remove(member: Element) -> Element? allViews.remove(cancelButton) // clearer

有时,重复类型信息是必要的,它可以避免混淆,但大多数时候,我们应该用一个单词来描述参数承担的角色而不是它的类型。具体参考下一条。

2.1.3 根据变量、参数以及关联类型(associated type)的角色为它们命名,而不是根据它们的类型约束为它们命名

例如:

var string = "Hello" protocol ViewController {      associatedtype ViewType : View } class ProductionLine { func restock(from widgetFactory: WidgetFactory) }

像上面这样把类型当成名称来用并不会提高代码的简洁性和可读性。我们应该基于一个实体的角色来为它命名,像这样:

var greeting = "Hello"  protocol ViewController {      associatedtype ContentView : View } class ProductionLine {      func restock(from supplier: WidgetFactory) }

如果associatedtypeprotocol关联性非常强,甚至protocol的名称就是associatedtype要承担的角色。此时,为了避免名称冲突,应该在associatedtype的名称末尾添加Type后缀,例如:

protocol Sequence {      associatedtype IteratorType : Iterator }
2.1.4 为弱类型信息参数承担的角色提供补偿

特别是一个参数的类型是NSObjectAnyAnyObject或者是IntString这样的基本类型时,类型自带信息和使用它们的上下文不能充分表达它们的使用意图。例如,对于下面这个例子来说,看声明是清晰的,但用起来,确是含糊的。

func add(observer: NSObject, for keyPath: String) grid.add(self, for: graphics) // vague

为了解决这个问题,在每个弱类型信息参数前面,加上一个表示角色的名词

func addObserver(_ observer: NSObject, forKeyPath path: String) grid.addObserver(self, forKeyPath: graphics) // clear

2.2 尽全力做到使用连贯

2.2.1 倾向于那些在使用时可以形成正确英语语法的名字

例如,下面这些表达方式是正确的:

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()

通常,对于那些在调用时,不影响方法核心语义的参数,为了表意的连贯,让他们的参数有更简单的形式也是可接受的。例如:

AudioUnit.instantiate(      with: description,      options: [.inProcess], completionHandler: stopProgressBar)
2.2.2 在工厂方法的名字前使用make

,例如:x.makeIterator()
2.2.2.1 对于构造函数和工厂方法来说,调用它们时形成的英文语句中不应包含第一个参数,例如,下面的代码是正确的:

let foreground = Color(red: 32, green: 64, blue: 128) let newPart = factory.makeWidget(gears: 42, spindles: 14)

在下面的代码中,API的作者尝试在方法被调用时,通过第一个参数的名字创建语法连贯的表达方式(但这样是错误的):

let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128) let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)

实际上,这条原则和下面对参数命名的原则一起,决定了绝大多数时候,(构造函数和工厂方法)的第一个参数总是会有external name的。除非,调用时体现的是一个“无损类型转换(full-width type conversion)”

let rgbForeground = RGBColor(cmykForeground)

2.2.2.2 根据函数或方法的副作用为它们命名

  • 没有副作用的方法或函数应该使用一个名词表达方式,例如:x.distance(to: y)i.successor()
  • 有副作用的方法或函数应该使用一个使役动词命名,例如:print(x)x.sort()x.append(y)
  • 总是成对命名mutating和non mutating方法。一个mutating方法通常会伴随一个non mutating变体,它们表达类似的语义,但是non mutating版本返回一个新对象,而不是在原有对象上就地处理;

    • 当方法原生通过动词表现时,用这个使役动词命名方法的mutating版本,在这个使役动词后面添加ed
      或ing
      后缀命名对应的non mutating版本;
      • 例如:mutating – x.sort(),non mutating – x.sorted()
      • 例如:mutating – x.append(y),non mutating – x.appedning(y)
    • 当方法原生通过名词表现时,使用名词来命名方法的non mutating版本,在名词前添加form定义mutating版本;
      • 例如:non mutating –x.union(z),mutating – y.formUnion(z)
      • 例如:non mutating – c.successor(i),mutating – y.formSuccessor(&i)

关于eding的补充说明
应优先考虑使用动词的过去分词为non mutating方法命名(通常是ed
结尾)

例如:

/// Reverses self in-place. mutating func reverse() /// Returns a reversed copy of self. func reversed() -> Self ... x.reverse() let y = x.reversed()

当动词带有一个直接宾语导致添加ed会造成语法错误时,使用动词的现在进行时(添加ing)为non mutating方法命名
例如:

/// Strips all the newlines from self mutating func stripNewlines()  /// Returns a copy of self with all the newlines stripped. func strippingNewlines() -> String ... s.stripNewlines() let oneLine = t.strippingNewlines()

2.2.2.3 使用Bool语义的方法或属性时,当这是一个non mutating操作时,它们读上去应该像是对调用对象的断言
例如:x.isEmptyline1.intersects(line2)

2.2.2.4 如果protocol用于为某种事物进行定义,这个protocol应该使用名词命名
例如:Collecion

2.2.2.5 如果protocol用于表达某种事物的能力,这个protocol的名字,应该使用ableibleing后缀
例如:EquatableProgressReporting

2.2.2.6 其它的类型、属性、变量和常量应该使用名词;

2.3 正确使用专业术语

Term of Art
Term of Art – 指一个在特定领域或专业里,有明确特殊含义的名词或短语(fn)。

2.3.1 避免使用复杂晦涩的单词

如果有更为常用的单词表达同样的含义,就避免使用复杂晦涩的单词。例如,(当我们要表达“表皮”这个含义的时候),如果“skin”已经充分表达意图了,就不要使用“epidermis”。虽然专业术语是重要的沟通方式,但只有需要表达重要含义,使用普通词会导致信息丢失的时候,才使用专业术语。

2.3.2 如果使用term of art,就始终坚持它在特定领域的含义

唯一一个需要使用专业术语,而不是普通单词的场景,就是需要严谨准确的表达某些含义,而使用非术语会带来歧义的时候。
因此,如果API中使用了专业术语,就要坚持使用这个术语被广为接受的含义(fn)。

  • 不要让专家感到意外:不要使用专业术语单词的其它含义,这会让特定领域的专业人员赶到意外,甚至愤怒;
  • 不要让初学者感到困惑:当一些初学者尝试理解专业术语时,他们可能会在Web上找到这些单词的非专业领域含义(注:言外之意也就是仅在必要的时候才使用专业术语);
2.3.3 避免使用缩写

尤其是对于那些仅在特定领域里才使用的缩写,他们就像特定人群使用的暗号一样,高效并且有局限性。是否能正确理解它们的含义,完全取决于是否可以正确的“猜”出缩写的各个单词。

因此,当一定要使用缩写时,至少让你的缩写可以很方便的在搜索引擎找到(没有歧义)的解释。

2.3.4 尊重专业习惯

不要以牺牲专业习惯为代价,尝试让新手更容易理解专业术语。例如:

  • 把一段(内存)连续的数据结构定义为Array比定义成List要好的多。尽管对于新手来说,List可能更容易表现出“一列事物”这样的概念。在现代计算机中,Array是很基础的概念,所以每一个开发者都应该知道,或者立即去了解,什么是一个Array。尝试使用开发者都熟知的词汇,初学者会在提问和使用搜索引擎时,更容易获得帮助;
  • 在某个特定领域里,例如数学,使用sin(x)表示正弦函数依然是众所
    周知的事情,使用诸如
    verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
    这样的方式反而让人觉得不知所云(尽管这是在解释什么是正弦)。另外,尽管我们刚刚说过避免使用缩写,却又使用了sin(x)替代了sine(x)。是的,这次,专业习惯在决策考量中占据了更大的比重。数十年来,任何和数学打交道的从业者们已然习惯了使用sin(x),我们应该尊重这个习惯。

第三部分 约定俗成

3.1 一般习俗
3.1.1 使用文档明确标注哪些算法复杂度不是O(1)的computed property

人们通常会认为访问一个computed property不会带来严重的性能损耗,因为在使用它们的时候,和使用一个stored property是感受不到区别的。因此,当computed property会引起性能问题时,要明确告知开发者。

3.1.2 相比自由函数(free function),更倾向于使用方法和属性解决问题

仅在一些特定的情况使用自由函数。例如:
1.要完成的任务不针对特定的self对象:min(x, y, z)
2.函数用于执行通用性功能:print(x)
3.调用函数的语法在特定领域里属于“习惯性用法”:sin(x)

3.1.3 使用以下命名方式

类型和protocols使用UpperCamelCase进行命名,其它都使用lowerCamelCase命名。对于“首字母缩略词(Acronyms and Initialisms)”则采用以下原则:

  • 美式英语中常用的由全大写字母构成的的“首字母缩略词”应该根据使用的上下文环境,统一使用大写或小写字母,例如:
var utf8Bytes: [UTF8.CodeUnit] var isRepresentableAsASCII = true var userSMTPServer: SecureSMTPServer
  • 其它的“首字母缩略词”应按照一般单词处理,例如:
var radarDetector: RadarScanner var enjoysScubaDiving = true
3.1.4 方法可以共享一个公共的名字

当方法表达相似的含义或它们用在不同的领域时,可以共享同一个名字。例如:

  • 下面的方法是被提倡的,因为它们本质上表达的含义是相同的:
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 { ... } }

并且,由于几何和集合属于两个不同的技术领域,因此,同一个程序中,在集合中使用contains也是没问题的:

extension Collection where Element : Equatable {     /// Returns `true` iff `self` contains an element equal to    /// `sought`.     func contains(sought: Element) -> Bool { ... } }

但是,在下面的例子中,不同的index方法表达完全不同的语义,它们应该使用不同的名字:

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 { ... } }

最后,避免“通过方法的返回值”实施重载行为,它们会在使用type inference的时候,带来歧义。

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? { ... } }

3.2 关于参数

func move(from start: Point, to end: Point)
3.2.1 让参数名为文档服务

尽管在函数或方法被调用的时候,参数名并不会出现,但它们对解释函数或方法的用途有重要作用。
因此,选择那些可以让文档更易读的参数名。例如,下面的例子里,注释文档读起来就很自然:

/// 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])
3.2.2 使用默认参数简化绝大多数的应用场景

如果一个参数在绝大多数时候都会使用同一个值,应该考虑为它指定默认值。
参数默认值可以通过隐藏次要信息提高代码的可读性,例如:

let order = lastName.compare(      royalFamilyName, options: [], range: nil, locale: nil)

(如果后三个参数都有默认值),compare调用读起来就会简单的多:

let order = lastName.compare(royalFamilyName)

另外,如果你之前使用过方法家族(method families),你就更应该考虑使用是否可以用带有默认参数的方法来替代它们。这不仅可以帮助开发者更容易了解API的用法,也可以减轻对“家族成员”的认知负担。例如:

extension String {      /// ...description...      public func compare(          other: String, options: CompareOptions = [],          range: Range? = nil, locale: Locale? = nil     ) -> Ordering }

尽管上面的compare看上去并不简单,但它总比下面的这个方法家族看上去简单多了:

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 }

方法家族中的每一个成员都需要单独的文档注释,都需要开发者去了解。为了能在调用时确认使用的成员,开发者不仅需要了解家族中的每一个方法,还偶尔会被foo(bar: nil)foo()之间的差别吓一跳。因此,在一堆“看上去都差不多”的方法中,指出到底调用了哪个方法是一件让人头疼的事情。而用一个方法,合理搭配上默认参数,可以极大改进开发体验。

3.2.3 倾向于从参数列表的末尾开始安排带有默认值的参数

通常,如果参数没有默认值,它会对方法的执行语义有更为重要的影响。并且,把带有默认值的参数放在最后,可以让方法的调用方式更为一致。

@3.3 关于参数Label

func move(from start: Point, to end: Point) x.move(from: x, to: y)
3.3.1 当区分参数没有意义时,忽略所有的参数label

例如在下面的两个方法里,区分参数时没意义的,它们的所有参数都不应该带有label:

min(number1, number2), zip(sequence1, sequence2)
3.3.2 如果初始化方法执行了不会带来信息丢失的类型转换,应该忽略初始化方法第一个参数的label

例如:Int64(someUint32)

如果函数的功能是执行类型转换,那么第一个参数应该总是源类型(source of conversion),例如:

extension String {      // Convert `x` into its textual representation in the given radix      init(_ x: BigInt, radix: Int = 10) ← Note the initial underscore } text = "The value is: " text += String(veryLargeNumber) text += " and in hexadecimal, it's" text += String(veryLargeNumber, radix: 16)

如果类型转换会带来信息丢失,应该在参数前面添加一个描述信息处理方式的label,例如:

extension UInt32 {      /// Creates an instance having the specified `value`.      init(_ value: Int16)            ← Widening, so no label      /// Creates an instance having the lowest 32 bits of `source`.      init(truncating source: UInt64)      /// Creates an instance having the nearest representable      /// approximation of `valueToApproximate`.      init(saturating valueToApproximate: UInt64) }
3.3.3 如果第一个参数在方法被调用时形成了介词短语,应该给第一个参数添加label

并且,参数的label也应该用介词开头,例如:x.removeBoxes(havingLength: 12)
但是这条也有例外:当方法的前两个参数一起表达一个动作的抽象时,应该忽略第一个参数的label前面的介词。
例如,下面的这两个例子,它们的第一个参数都不应该以介词开头:

a.move(toX: b, y: c) a.fade(fromRed: b, green: c, blue: d)

遇到这种情况时,我们应该把介词放到参数label前面,这样可以更清楚的表达要执行的动作,例如:

a.moveTo(x: b, y: c) a.fadeFrom(red: b, green: c, blue: d)
3.3.4 否则,如果第一个参数(在方法调用时)形成了一个语法正确的短语,就忽略第一个参数的label

并且,应该在方法名后面添加必要的前置单词。例如:x.addSubview(y)
这条指南的另外一个含义是,如果第一个参数不能在调用时和函数名形成语法正确的短语,它就应该有一个label,例如:

view.dismiss(animated: false) let text = words.split(maxSplits: 12) let studentsByName = students.sorted(      isOrderedBefore: Student.namePrecedes)

这里还有一点要强调的是,在调用时(忽略掉第一个参数的label)形成的短语不仅要语法正确,还要表达正确、无歧义的语义。例如,下面这些例子,虽然语法正确,但却是有歧义的(因此,我们不应该忽略第一个参数的label):

view.dismiss(false)      Don't dismiss? Dismiss a Bool? words.split(12)          Split the number 12
3.3.5(除了第一个参数之外),为所有其它参数设置label

第四部分 特殊情况

4.1 在你的API里,为closure的参数和tuple成员设置label

为这些元素设置的label好处有二:
1.可以极大提升文档注释的表达能力;
2.让访问tuple成员的代码更易读;

例如:

/// Ensure that we hold uniquely-referenced storage for at least /// `requestedCapacity` elements. /// /// If more storage is needed, `allocate` is called with /// `byteCount` equal to the number of maximally-aligned /// bytes to allocate. /// /// - Returns: /// - reallocated: `true` iff a new block of memory /// was allocated. /// - capacityChanged: `true` iff `capacity` was updated. mutating func ensureUniqueStorage(      minimumCapacity requestedCapacity: Int,      allocate: (byteCount: Int) -> UnsafePointer<Void> ) -> (reallocated: Bool, capacityChanged: Bool)

尽管从技术上来说,我们在closure中使用的是参数label(这里指byteCount),但在注释文档中,我们应该把它当做参数名称来使用。
在函数内部调用这个closure时,这个调用形成的表达方式,和调用一个“名称中不包含第一个参数(注:这应该是相对于名称中会带有介词的函数名而言的)”的函数是一致的:
allocate(byteCount: newCount * elementSize)

4.2 留意那些无约束类型带来的多态效果(unconstrained polymorphism)

特别是AnyAnyObject
或者泛型参数,它们很容易在方法重载时意外带来歧义。例如,考虑下面两个重载的方法:

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<          S : SequenceType where S.Generator.Element == Element     >(newElements: S) }

这两个方法形成了一个语义家族(semantic family),它们的参数看上去是截然不同的。但是,当ArrayElement
的类型是Any时,一个单一的元素有可能和一个元素集合的类型是相同的。
例如,在下面的例子里,我们究竟应该认为[2, 3, 4]是一个Any,还是把它理解为是一个[Any]呢?

var values: [Any] = [1, "a"] values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?

为了避免这个歧义,我们可以给第二个重载版本的参数,添加一个label:

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<  S : SequenceType where S.Generator.Element == Element  > (contentsOf newElements: S)}

注意到新添加的参数label是如何和文档注释匹配的了么?这样,编写文档注释实际上也是在提醒作者每个API的用途。

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Swift 3 SE-0023 API设计指南

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址