Swift 3.0 API設計準則
這些正在開發的API指南草案是 Swift 3.0 effort 的一部分。
全文目錄
基本要素
-
使用要點要清晰
-
清晰比簡潔更重要
-
寫明文檔注釋
命名
-
命名更加明確
-
符合語法規則
-
善用專業術語
約定
-
常規約定
-
參數
特別說明
------------------------------------------------------------------------------------------------------------------------------------------------
基本要素
-
把能夠清晰使用作為你設計時最重要的目標。因為代碼的可讀性比代碼自身更重要。
-
代碼的清晰邏輯性比代碼的簡潔性更重要。Swift代碼的簡潔性,不是指使用最少的字符來實現程序代碼。Swift編程的簡潔性帶來的一個副作用是由強類型系統和減少引用文件的特性決定的。
-
使用Swift的標記語法,為每一個方法和屬性寫注釋性文本。最理想的情況,開發者能夠從注釋的簽名和一兩句總結中明白API的使用和意義。
/// Returns the first index where `element` appears in `self`,
/// or `nil` if `element` is not found.
///
/// - Complexity: O(`self.count`).
public func indexOf(element: Generator.Element) -> Index? { 在初步設計時,編寫注釋性文檔是一個好的主意,因為這能使你對API設計有更深入地理解,從而有利用于API的進一步設計。
注意:設計的API應簡潔可描述,否則,該API很有可能是有問題的。
命名規則
清晰表達
-
所有用來命名的單詞,都應該避免使代碼閱讀人產生歧義。
例如,定義一個方法,該方法可以刪除集合中指定位置的元素。
public mutating func removeAt(position: Index) -> Element
調用方式:
employees.removeAt(x)
如果我們省略該方法名中的單詞At,就可能會讓讀者誤認為,這個方法的作用是搜索并且刪除指定元素X而不是刪除集合中X位置處的元素。
employees.remove(x) // unclear: are we removing x ?
-
省略不必要的單詞。每一個被用來命名的單詞都應傳達精確的信息。
有時,為了命名清晰或消除歧義,可能需要使用很多的詞語,但是那些讀者已經明確這些表達意圖的話,那么這個詞語就會變得冗余,此時就應該省略掉這些冗余的詞語。尤其是那些僅僅只重復類型信息的詞更應被省略:
public mutating func removeElement(member: Element) -> Element? allViews.removeElement(cancelButton)
這個例子中,remove后Element沒有使表達更清晰,因此是冗余的。如下API的表述就更好:
public mutating func remove(member: Element) -> Element? allViews.remove(cancelButton) // clearer
少數情況下,為了避免產生歧義,重復信息也是有必要的;但一般情況下,用一個詞語而不是一個類型來描述參數的作用會更好。有關詳細信息,請參閱下一項。
-
為了清晰表述參數的作用需要對弱類型信息進行補充。
特別是當參數的類型是NSObject、Any、AnyObject或者是一個基本類型如Int或者String時,類型信息便不能充分表達參數的使用目的。如下面的例子,聲明是明確的,但在調用的時候是有些含糊不清的:
func add(observer: NSObject, for keyPath: String) grid.add(self, for: graphics) // vague
為了使表述更清晰,就需要在每一個弱類型參數前加一個名詞來描述它的作用:
func addObserver(_ observer: NSObject, forKeyPath path: String) rid.addObserver(self, forKeyPath: graphics) // clear
符合語法規則
-
對動態方法進行命名時,應該使用動詞短語,如:
x.reverse(), x.sort(), x.append(y).
-
對靜態方法進行命名時,應該使用名詞短語,如:
x.distanceTo(y), i.successor().
-
當名詞不能很好地表述時,使用動詞來命名也是可以接受的:
let firstAndLast = fullName.split() // acceptable
當一個動態方法是用動詞來表述時,那么可以用對該動詞的過去時或者進行時形式(如”ed/ing”形式),來對該動態方法對應的靜態方法進行命名。如:
x.sort()和x.append(y)和靜態形式則為:x.sorted()和x.appending(y)。
通常情況下,一個動態方法都會有一個不同形式的靜態方法,并且該靜態方法的返回值與動態方法的返回值的類型相似或者相同。
對靜態方法進行命名時,優先使用動詞的過去時態(一般為“ed”形式)
/// Reverses `self` in-place. mutating func reverse() /// Returns a reversed copy of `self`. func reversed() -> Self ... x.reverse() let y = x.reversed()
當動詞后接直接賓語時,用過去時態就不符合語法規則。此時,應用動名的進行時態(一般為“ing”形式)來對靜態方法進行命名。
/// 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()
-
對布爾類型的靜態方法和屬性進行命名時,應該使用斷言性詞語,如: x.isEmpty, line1.intersects(line2)
-
對屬性描述性協議進行命名時,應該使用名詞。對能力描述性協議進行命名時,應該使用帶后“able”、“ible”或者“ing”等后綴的詞(如:Equatable、ProgressReporting)
-
其他類型,屬性,變量和常量,都應該用名詞進行命名。
善用專業術語
專業術語:在特定領域或專業,有明確的,特殊含義的詞或短語
-
如果常用詞就能表達相同或者相近的含義,那么就不要用生澀的詞語。如當“skin”能很好表達含義時,就不應用“epidermis”來表達。雖然專業術語是必不可少的交流工具,但也只能用來描述關鍵的含義,否則將失去其原有的含義。
-
應該遵循公認的含義:如果需要使用專業術語的話,請使用其常見的那個意思,避免產生不必要的誤解。
當常用詞不能準確表達其含義,或者使用常用詞會導致其含義模糊不清時,才能使用專業術語。因此,應該根據所能接受的含義嚴格使用API專業術語。
1.不要獨樹一幟:以為創造出了一種新的含義,實際上只會讓對這個術語了如指掌的專家感到奇怪甚至憤怒。
2.不要誤導新手:每個學習此新術語的人,都會到網上查詢術語的意思,以便進行理解,他們肯能看到的是其傳統意思。
-
避免縮寫:不標準的縮寫會對這個術語造成很大的影響,因為對該縮寫有效的理解取決于其對應的完整形式。
使用的任何縮寫都應該是能在網上搜到其含義的
-
維持原意:不要為了讓初學者更好理解,對專業述語進行優化而使其失去原有含義。
在連續數據結構進行命名時,雖然初學者可能更容易理解List,但使用Array比使用List要好。因為數組是現代計算的基礎,所以每個程序員都知道或者將會知道array代表什么。使用大部分程序員都熟知的術語,這樣也便于他們在網絡進行查詢和提問時能夠更快得到反饋。
在某些特地編程領域里面,例如數學運算,一個廣泛的先例術語:sin(x),顯然比下面解釋其含義的描述語句更好:
verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
注意,在這種情況下,相比專業述語,先例詞更應該謹慎縮寫:雖然完整的單詞是“sine”,但幾十年來,編程人員一直用的都是“sin(x)”,而數學家們甚至使用了好幾個世紀。
約定
常規約定
-
文檔的任何計算屬性復雜度不能為O(1), 人們經常假設屬性訪問不涉及明顯的計算,因為這些屬性是以抽象模型形式存諸起來的。但當這種假設不成立時, 務必要提醒他們注意。
-
優先使用方法或屬性函數,而不是自由函數(free functions),因為自由函數僅在一些特殊情況才能使用:
1.沒有明顯的self關鍵字:
min(x, y, z)
2.函數為一個通用的泛型:
print(x)
3.函數的語法是已存在域符號的一部分:
sin(x)
-
遵循范例約定:類型,協議和枚舉都是UpperCamelCase(大駝峰命名規則)。其他的都是按照lowerCamelCase(小駝峰命名規則)。
-
當方法都是表達相同基本含義時,它們可以共用一個基本名稱,但是他們的運算必須是不同類型,或者作用在不同的域里面。
例如,下面這種表述是正確的,因為方法在本質上都是處理相同的事情:
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 { ... }
} 但是,下面的這些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 { ... }
} 最后,不要使用返回值類型重載函數,這樣使用會導致Swift在類型推到的時候,產生歧義:
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? { ... }
} 參數
-
當簡化共用時,要充分利用默認參數。任何一個只有單一常用值的參數都可以默認參數。
默認參數通過隱藏一些不相關信息來提高可讀性。例如:
let order = lastName.compare( royalFamilyName, options: [], range: nil, locale: nil)
更簡單的寫法:
let order = lastName.compare(royalFamilyName)
默認參數通常優于方法簇,因為默認參數的使用為學習API的人減少認知上的負擔。
extension String {
/// *...description...*
public func compare(
other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
) -> Ordering
} 上面這種表述可能有點復雜,但是它比下面更簡潔明了:
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
} 方法簇中每一個成員,都需要用戶單獨編寫和理解。如果使用它們,用戶需要去了解每一個方法,有時候還要理清楚它們之間的關聯。例如,fooWithBar(nil)和foo()方法并不總是同義的---從一個幾乎完全相同的定義中去尋找它們之間細微的差別,是一個很繁瑣的過程。從很多優秀編程人員的經驗得知,應該使用一個帶有默認參數的單一方法,而不是方法簇。
-
最好將帶有默認值的參數放在參數列表的末尾。不帶默認值的參數對于方法本身來說更重要,當被調用時提供一個穩定的初始化模式。
-
優先使用Swift語言默認外部參數標簽。
換言之:
1.方法或者函數的第一個參數不需要參數標簽
2.方法或者函數的其他參數都必須要參數標簽
3.所有參數的初始化模塊也需要參數標簽
對應上面所說,如果每個參數都是像下面這種定義方式,那么所有參數也需要參數標簽:
identifier: Type
只有少數幾個例外情況:
1.對于無損“full-width”(即占用空間小的類型向占用空間大的類型轉換)類型轉換的構造器方法而言,第一個參數應該是待轉換的類型,并且這個參數不應該寫有外部參數標簽。
extension String {
// Convert `x` into its textual representation in the given radix
init(_ x: BigInt, radix: Int = 10) // Note the initial separate underscore
}
text = "The value is: "
text += String(veryLargeNumber)
text += " and in hexadecimal, it's"
text += String(veryLargeNumber, radix: 16) 對于有損“narrowing”(即占用空間大的類型向占用空間小的類型轉換)類型轉換,推薦使用外部參數標簽來描述這個特定類型:
extension UInt32 {
init(_ value: Int16) // widening, so no label
init(truncating bits: UInt64)
init(saturating value: UInt64)
} 3.當所有的參數都是相同的類型,不能有效區分的時候,也不應該使用參數標簽。例如幾個常見的例子: min(number1,number2)、zip(sequence1,sequence2)。
4.當第一個參數不可用時,這時候需要一個明顯的參數標簽。
extension Document {
func close(completionHandler completion: ((Bool) -> Void)? = nil)
}
doc1.close()
doc2.close(completionHandler: app.quit) 正如你所看到的上面這個例子,方法可以都正確調用,不管參數是否被顯式的傳入。如果我們將參數的描述缺省,調用的時候可能有錯誤暗示:參數是語句的直接賓語:
extension Document {
func close(completion: ((Bool) -> Void)? = nil)
}
doc.close(app.quit) // Closing the quit function? 如果你把參數的描述加到函數名上面,當函數默認被調用時,可能就會產生歧義:
extension Document {
func closeWithCompletionHandler(completion: ((Bool) -> Void)? = nil)
}
doc.closeWithCompletionHandler() // What completion handler? 特別說明
在重載集合中,需要特別注意無約束多態類型(如Any、AnyObjecty以及無約束泛型參數)來避免歧義。
例如,我們看下面這個重載:
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)
} 這些方法構成了一個語義族,在第一個方法中參數類型有很大的不同,然而當Element為Any類型時,一個單個元素擁有和一系列元素相同的類型:
var values: [Any] = [1, "a"] values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
想要去除歧義,第二個重載方法名應該更加明確:
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 appendContentsOf<
S : SequenceType where S.Generator.Element == Element
>(newElements: S)
} 注意,這個新方法的名稱能更好的匹配文檔的注釋。在這個例子中,編寫文檔的注釋實際上可以讓編寫API的設計者更多關注于問題的本質。
-
使用更加友好的文檔注釋工具。在些工具可以自動幫我們提取、生成格式化的公共文檔(API參考文檔),這些文檔會出現在Xcode,生成的接口(interfaces),快速幫助文檔和代碼補全提示中。
我們使用的Markdown編輯器能夠便捷的給我們生成一個如下整齊的列表:
-
-Attention: -Important: -Requires:
-
-Author: -Invariant: -See:
-
-Authors: -Note: -Since:
-
-Bug: -Postcondition: -Throws:
-
-Complexity: -Precondition: -TODO:
-
-Copyright: -Remark: -Version:
-
-Date: -Remarks: -Warning:
-
-Experiment: -Returns:
編寫一篇很棒的總結,相對來說比使用關鍵字,更為重要。
如果方法的簽名和方法頭注釋行,已經描述了參數或者返回值類型信息,那么你沒有必要去為它們編寫一個注釋性的文檔來描述它們的用法或作用,如下:
/// Append `newContent` to this stream. mutating func write(newContent: String)
-
本文僅用于學習和交流目的,轉載請注明文章譯者、出處和本文鏈接。
-
感謝 博文視點 對本期活動的支持