Swift API 設(shè)計(jì)指南(下)

接上篇:Swift API 設(shè)計(jì)指南(上)

合理使用術(shù)語(yǔ)(Terminology)

  • 如果一個(gè)常用的詞可以清楚地表達(dá)意圖,就不要使用晦澀難懂的術(shù)語(yǔ)。用 “skin” 就可以達(dá)到你的目的的話,就別用 “epidermis”了。術(shù)語(yǔ)是一種非常必要的交流工具,但是應(yīng)該用在其他常用語(yǔ)無(wú)法表達(dá)關(guān)鍵信息的時(shí)候。

  • 如果你確實(shí)要使用術(shù)語(yǔ),請(qǐng)確保它已被明確定義
    使用術(shù)語(yǔ)的唯一理由是,用其他常用詞會(huì)表意不清或造成歧義。因此,API 應(yīng)該嚴(yán)格依據(jù)某個(gè)術(shù)語(yǔ)被廣泛接受的釋義來(lái)進(jìn)行命名。

    • 不要讓專家驚訝:如果我們重新定義了某個(gè)術(shù)語(yǔ),那會(huì)使熟知它的人感到驚訝甚至是憤怒。
    • 不要讓初學(xué)者困惑:初學(xué)某個(gè)術(shù)語(yǔ)的人通常都會(huì)上網(wǎng)搜索它的傳統(tǒng)釋義。
  • 避免縮寫??s寫,特別是不標(biāo)準(zhǔn)的縮寫,已經(jīng)算是一種術(shù)語(yǔ)了,因?yàn)橐斫馑囊馑急仨氄_地把它翻譯成完整版本才行。

你使用的所有縮寫,必須可以很輕易的上網(wǎng)查到它的意思。

  • 有例可循。不要為一個(gè)新人去優(yōu)化術(shù)語(yǔ),而不遵守現(xiàn)有的規(guī)范。
    將一個(gè)線性的數(shù)據(jù)結(jié)構(gòu)命名為Array比一些更簡(jiǎn)單的詞(譬如List)要好,盡管List對(duì)新手來(lái)說(shuō)更易于理解。因?yàn)閿?shù)組在現(xiàn)代計(jì)算機(jī)體系中是個(gè)非?;A(chǔ)的概念,每個(gè)程序員都已經(jīng)知道或者能夠很快地學(xué)會(huì)它??傊?qǐng)使用那些為程序員所熟知的術(shù)語(yǔ),這樣當(dāng)人們搜索和詢問(wèn)時(shí)就能得到回應(yīng)。
    在一些特定的編程領(lǐng)域,譬如數(shù)學(xué)運(yùn)算方面,廣為人知的sin(x)就比解釋性的短語(yǔ)(如verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x))要好得多。注意,在這種情況下,“有例可循”的優(yōu)先級(jí)大于指南中的“不要使用縮寫”,哪怕完整的詞是sine。畢竟“sin(x)”已經(jīng)被程序員們用了幾十年了,更被數(shù)學(xué)家們用了幾個(gè)世紀(jì)了。

約定

通用約定

  • 標(biāo)注那些復(fù)雜度不是 O(1) 的計(jì)算屬性。人們總是假定存取屬性是不需要什么計(jì)算的,因?yàn)樗麄円呀?jīng)把屬性保存為心智模型( mental model)了。

  • 盡量使用方法和屬性,而不是自由函數(shù)(全局函數(shù))。自由函數(shù)僅適用于一些特定情況:

    1. 當(dāng)沒(méi)有明顯的selfmin(x, y, z)
    2. 當(dāng)函數(shù)是無(wú)約束的范型(unconstrained generic):print(x)
    3. 當(dāng)函數(shù)句法(syntax)是權(quán)威認(rèn)證的領(lǐng)域標(biāo)記的一部分:sin(x)
  • 遵守拼寫約定(case conventions)。類型和協(xié)議用首字母大寫駝峰命名法(UpperCamelCase)命名,其它的都用首字母小寫(lowerCamelCase)駝峰命名法。
    在英語(yǔ)中都是大寫字母的首字母縮略詞(Acronyms and initialisms)需要根據(jù)首字母情況統(tǒng)一成全大寫或者全小寫:

var utf8Bytes: [UTF8.CodeUnit];
var isRepresentableAsASCII = true;
var userSMTPServer: SecureSMTPServer;

其它首字母縮略詞當(dāng)作普通單詞處理即可:

var radarDetector: RadarScanner;
var enjoysScubaDiving = true;
  • 當(dāng)幾個(gè)方法的基本意義相同,或者它們作用在明確的范圍內(nèi)時(shí),可以共享同一個(gè)基本命名
    比如,下面的做法是被鼓勵(lì)的,因?yàn)檫@幾個(gè)方法基本做了相同的事情:
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 { ... }
}

由于范型和容器都有各自獨(dú)立的范圍,所以在同一個(gè)程序里像下面這樣使用也是可以的:

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

然而,如下這些index方法有不同的語(yǔ)義,應(yīng)該采用不同的命名:

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

最后,避免參數(shù)重載,因?yàn)檫@在類型推斷時(shí)會(huì)產(chǎn)生歧義:

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

參數(shù)

func move(from start: Point, to end: Point)
  • 選擇能服務(wù)于文檔(documentation)編寫的參數(shù)。雖然參數(shù)名不在方法的調(diào)用處顯示,但它們起到了非常重要的解釋說(shuō)明作用。
    選擇能使文檔通俗易懂的參數(shù)名。比如,下面這些參數(shù)名就是文檔讀上去很自然:
  /// 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])

而下面這些就使文檔難以理解且不合語(yǔ)法:

  /// 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])
  • 當(dāng)默認(rèn)參數(shù)能簡(jiǎn)化常見(jiàn)調(diào)用的時(shí)候好好利用它。任何有一個(gè)常用值的參數(shù)都可以使用默認(rèn)參數(shù)。
    通過(guò)隱藏次要信息,默認(rèn)參數(shù)提高了代碼可讀性,比如:
let order = lastName.compare(
  royalFamilyName, options: [], range: nil, locale: nil)

可以更加簡(jiǎn)潔:

let order = lastName.compare(royalFamilyName)

一般來(lái)說(shuō),默認(rèn)參數(shù)比方法族(method families)更可取,因?yàn)樗鼫p輕了 API 使用者的認(rèn)知負(fù)擔(dān)。

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

上面的代碼可能不算簡(jiǎn)單,但它比如下的代碼簡(jiǎn)單多了:

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
}

方法族中的每個(gè)方法都需要被分別注釋和被使用者理解。決定使用哪個(gè)方法之前,使用者必須理解所有方法,并且偶爾會(huì)對(duì)它們之間的關(guān)系感到驚訝,譬如,foo(bar: nil)foo()并不總是同義的,從幾乎相同的文檔和注釋中去區(qū)分這些微笑差異是十分乏味的。而一個(gè)有默認(rèn)參數(shù)的方法將提供上等的編碼體驗(yàn)。

  • 盡量把默認(rèn)參數(shù)放在參數(shù)列表的最后,沒(méi)有默認(rèn)值的參數(shù)通常對(duì)于方法的語(yǔ)義來(lái)說(shuō)是必要的,在方法被調(diào)用的時(shí)候提供了穩(wěn)定的初始形態(tài)。

參數(shù)標(biāo)簽

func move(from start: Point, to end: Point)
x.move(from: x, to: y) 
  • 當(dāng)不同參數(shù)不能被很好地區(qū)分時(shí),刪除所有參數(shù)標(biāo)簽,譬如:
min(number1, number2)
zip(sequence1, sequence2)
  • 當(dāng)構(gòu)造器完全只是用作類型轉(zhuǎn)換的時(shí)候,刪除第一個(gè)參數(shù)標(biāo)簽,譬如:Int64(someUInt32)。
    第一個(gè)參數(shù)應(yīng)該是轉(zhuǎn)換的源頭。
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)

然而,在一個(gè)“narrowing”的轉(zhuǎn)換中(譯者注:范圍變窄的轉(zhuǎn)換,譬如 Int64 轉(zhuǎn) Int32),用一個(gè)標(biāo)簽來(lái)表明范圍變窄是推薦的做法。

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)
}
  • 當(dāng)?shù)谝粋€(gè)參數(shù)是介詞短語(yǔ)的一部分時(shí),給它一個(gè)參數(shù)標(biāo)簽。標(biāo)簽應(yīng)該正常地以介詞開(kāi)頭,譬如:
x.removeBoxes(havingLength: 12)

頭兩個(gè)參數(shù)各自相當(dāng)于某個(gè)抽象的一部分的情況,是個(gè)例外:

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

在這種情況下,為了保持抽象清晰,參數(shù)標(biāo)簽從介詞后面開(kāi)始。

a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
  • 另外,如果第一個(gè)參數(shù)是符合語(yǔ)法規(guī)范的短語(yǔ)的一部分,刪除它的標(biāo)簽,在方法名后面加上前導(dǎo)詞,譬如:x.addSubview(y)
    這條指南暗示了如果第一個(gè)參數(shù)不是符合語(yǔ)法規(guī)范的短語(yǔ)的一部分,它就應(yīng)該有個(gè)標(biāo)簽。
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)

注意,短語(yǔ)必須表達(dá)正確的意思,這非常重要。如下的短語(yǔ)是符合語(yǔ)法的,但它們表達(dá)錯(cuò)誤了。

view.dismiss(false)   Don't dismiss? Dismiss a Bool?
words.split(12)       Split the number 12?

注意,默認(rèn)參數(shù)是可以被刪除的,在這種情況下它們都不是短語(yǔ)的一部分,所以它們總是應(yīng)該有標(biāo)簽。

  • 給其它所有參數(shù)都加上標(biāo)簽。

特別說(shuō)明

  • 在 API 中給閉包參數(shù)和元組成員加上標(biāo)簽
    這些名字有解釋說(shuō)明的作用,可以出現(xiàn)在文檔注釋中,并且給元組成員一個(gè)形象的入口。
/// 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)

用在閉包中時(shí),雖然從技術(shù)上來(lái)說(shuō)它們是參數(shù)標(biāo)簽,但你應(yīng)該把它們當(dāng)做參數(shù)名來(lái)選擇和解釋(文檔中)。閉包在方法體中被調(diào)用時(shí)跟調(diào)用方法時(shí)是一致的,方法簽名從一個(gè)不包含第一個(gè)參數(shù)的方法名開(kāi)始:

allocate(byteCount: newCount * elementSize)
  • 要特別注意那些不受約束的類型(譬如,Any,AnyObject和一些不受約束的范型參數(shù)),以防在重載時(shí)產(chǎn)生歧義。
    譬如,考慮如下重載:
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)
}

這些方法組成了一個(gè)語(yǔ)義族(semantic family),第一個(gè)參數(shù)的類型是明確的。但是當(dāng)ElementAny時(shí),一個(gè)單獨(dú)的元素和一個(gè)元素集合的類型是一樣的。

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

為了避免歧義,讓第二個(gè)重載方法的命名更加明確。

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

注意新命名是怎樣更好地匹配文檔注釋的。在這種情況下,寫文檔注釋時(shí)實(shí)際上也在提醒 API 作者自己注意問(wèn)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容