本文翻譯自蘋果官方文檔:Swift API Design Guidelines,如有錯漏,歡迎指出。
基本準則
在調用處表意足夠明確是你最重要的目的。像方法和屬性這樣的實體(Entities)只聲明一次,但卻會被重復調用,所以你需要設計好你的 API 讓它們可以被明確和簡潔的調用。當我們評價某個設計的時候,往往需要查看它的使用場景以確保它在實際環境中足夠明確,而不僅僅是看一眼它的聲明。
** 明確比簡潔更重要。**雖然 Swift 代碼可以寫得非常簡潔,但是通過減少字符數使得代碼盡可能簡短卻從不是我們的目標。在 Swift 中,簡潔只是強類型系統和其它可以減少樣板代碼的特性所帶來的一個副作用(side-effect)。
為每個聲明編寫文檔注釋。寫文檔時的感悟會對你的設計產生重大影響,所以不要擱置它。
命名
促使能被明確調用
-
包含所有需要的單詞,以避免人們在閱讀調用處的代碼時感到困惑。
譬如,有一個方法,要在集合(collection)中移除指定位置的元素。
推薦:
extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
如果我們刪掉方法簽名中的at
,那就給人一種該方法是搜索并刪除集合中等于x
的元素的感覺,而不是用x
來指示元素在集合中的位置,并把該位置的元素刪除。
不推薦:
employees.remove(x) // unclear: are we removing x?
-
刪除不需要的單詞。名字中的每個單詞都應該在調用處傳達出重點信息。
更多的單詞或許能澄清意圖和消除歧義,但是那些讀者已經知道的冗余信息都可以刪掉,尤其是那些僅僅重復了類型信息的單詞。
不推薦:
public mutating func removeElement(member: Element) -> Element?
allViews.removeElement(cancelButton)
上述情況下,Element
在調用處沒有提供任何要點信息,如下 API 會更好。
推薦:
public mutating func remove(member: Element) -> Element?
allViews.remove(cancelButton) // clearer
個別情況下,重復類型信息對于消除歧義是必要的,但一般來說,用一個表明參數角色(role)而不是類型的詞,會更好一些。詳情請參看下一條。
基于變量、參數、關聯類型的角色來對它們進行命名,而不是基于它們的類型。
不推薦:
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)
}
如果某個關聯類型和它的協議聯系非常緊密,以至于它的協議名就是它的角色名,那就給關聯類型的名字加上Type
避免沖突:
protocol Sequence {
associatedtype IteratorType : Iterator
}
-
為弱類型信息的參數添加補充信息以表明參數的角色
當參數類型是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)
grid.addObserver(self, forKeyPath: graphics) // clear
為能被流暢調用而努力
-
盡量使方法或函數名在調用的時候符合英語語法規范。
推薦:
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)
工廠方法的命名以
make
開頭,譬如:x.makeIterator()
。構造方法和工廠方法在調用時應該從一個不包含 first argument(譯者注:翻譯成第一個參數在這里好像不對頭,索性就不翻了,大家根據下面的例子應該可以理解它的意思)的短語開始,譬如:
x.makeWidget(cogCount: 47)
。
舉個例子,如下這些調用的短語都不包含 first argument。
推薦:
let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
而下面這段代碼的 API 作者企圖用 first argument 創建符合英語語法的順暢 API:
不推薦:
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
事實上,本指南包含了參數標簽(argument labels,譯者注:應該和外部參數名一個意思吧)這樣的的主題,意味著第一個參數都應該包含一個標簽,除非該方法完全只是用來做類型轉換的。
推薦:
let rgbForeground = RGBColor(cmykForeground)
-
基于函數和方法的副作用對它們命名
- 沒有副作用的方法讀起來應該是一個名詞詞組,譬如:
x.distance(to: y)
,i.successor()
。 - 有副作用的方法讀起來應該是一個命令式的動詞短語,譬如:
print(x)
,x.sort()
,x.append(y)
。 - 使用 “ed/ing” 規則對一個可變方法(mutating method)的不可變版本命名。
一般來說,可變方法都會有一個對應的不可變版本,該方法會返回一個和接受值相同或者相似類型的值。- 傾向于用過去分詞對不可變版本命名(一般是加 “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()
調用返回值為布爾型的不可變方法和屬性的時候讀起來應該是調用者的斷言(assertions),譬如:
x.isEmpty
,line1.intersects(line2)
。用來描述是什么的協議讀起來應該是個名詞。**(譬如:
Collection
)。用來描述能做什么的協議應該加上 able、ible 或者 ing 進行命名**(譬如:
Equatable
,ProgressReporting
)。其它類型、屬性、變量和約束的命名都應該用名詞。