Swift 中如何使用 Option Pattern 改善可選項(xiàng)的 API 設(shè)計(jì)
SwiftUI 中提供了很多“新穎”的 API 設(shè)計(jì)思路和 Swift 的使用方式,我們可以進(jìn)行借鑒,并反過來使用到普通的 Swift 代碼中。PreferenceKey 的處理方式就是其中之一:它通過 protocol 的方式,為子 view 們提供了一套模式,讓它們能將自定義值以類型安全的方式,向上傳到父 view 去。如果有機(jī)會,我會再專門介紹 PreferenceKey,但這種設(shè)計(jì)的模式其實(shí)和 UI 無關(guān),在一般的 Swift 里,我們也能使用這種方法來改善 API 設(shè)計(jì)。
在這篇文章里,我們就來看看要如何做。文中相關(guān)的代碼可以在這里找到。你可以將這些代碼復(fù)制到 Playground 中執(zhí)行并查看結(jié)果。
紅綠燈
用一個(gè)交通信號燈作為例子。

作為 Model 類型的 TrafficLight 類型定義了 .stop、.proceed 和 .caution 三種 State,它們分別代表停止、通行和注意三種狀態(tài) (當(dāng)然,通俗來說就是“紅綠黃”,但是 Model 不應(yīng)該和顏色,也就是 View 層級相關(guān))。它還持有一個(gè) state 來表示當(dāng)前的狀態(tài),并在設(shè)置時(shí)將這個(gè)狀態(tài)通過 onStateChanged 發(fā)送出去:
public class TrafficLight {
public enum State {
case stop
case proceed
case caution
}
public private(set) var state: State = .stop {
didSet { onStateChanged?(state) }
}
public var onStateChanged: ((State) -> Void)?
}
其余部分的邏輯和本次主題無關(guān),不過它們也比較簡單。如果你有興趣的話,可以點(diǎn)開下面的詳情查看。但這不影響本文的理解。
TrafficLight 的其他部分
在 (ViewController 中) 使用這個(gè)紅綠燈也很簡單。我們按照紅綠黃的顏色,在 onStateChanged 中設(shè)定 view 的顏色:
light = TrafficLight()
light.onStateChanged = { [weak self] state in
guard let self = self else { return }
let color: UIColor
switch state {
case .proceed: color = .green
case .caution: color = .yellow
case .stop: color = .red
}
UIView.animate(withDuration: 0.25) {
self.view.backgroundColor = color
}
}
light.start()
這樣,View 的顏色就可以隨著 TrafficLight 的變化而變更了:

青色信號
世界很大,有些地方 (比如日本) 會使用傾向于青色,或者實(shí)際上應(yīng)該是綠松色 (turquoise),來表示“可以通行”。有時(shí)候這也是技術(shù)的限制或者進(jìn)步所帶來的結(jié)果。
The green light was traditionally green in colour (hence its name) though modern LED green lights are turquoise.
– Wikipedia 中關(guān)于 Traffic light 的記述

假設(shè)我們想要讓 TrafficLight 支持青色的綠燈,一個(gè)能想到的最簡單的方式,就是在 TrafficLight 里為“綠燈顏色”提供一個(gè)選項(xiàng):
public class TrafficLight {
public enum GreenLightColor {
case green
case turquoise
}
public var preferredGreenLightColor: GreenLightColor = .green
//...
}
然后在 ViewController 中使用對應(yīng)的顏色:
extension TrafficLight.GreenLightColor {
var color: UIColor {
switch self {
case .green:
return .green
case .turquoise:
return UIColor(red: 0.25, green: 0.88, blue: 0.82, alpha: 1.00)
}
}
}
light.preferredGreenLightColor = .turquoise
light.onStateChanged = { [weak self, weak light] state in
guard let self = self, let light = light else { return }
// ...
// case .proceed: color = .green
case .proceed: color = light.preferredGreenLightColor.color
}
這樣做當(dāng)然能夠解決問題,但是也會帶來一些隱患。首先,需要在 TrafficLight 中添加一個(gè)額外的存儲屬性 preferredGreenLightColor,這使得 TrafficLight 示例所使用的內(nèi)存開銷增加了。在上例中,額外的 GreenLightColor 屬性將會為每個(gè)實(shí)例帶來 8 byte 的開銷。 如果我們需要同時(shí)處理很多 TrafficLight 實(shí)例,而其中只有很少數(shù)需要 .turquoise 的話,這個(gè)開銷就非常可惜了。
嚴(yán)格來說,上例的 TrafficLight.GreenLightColor 枚舉其實(shí)只需要占用 1 byte。但是 64-bit 系統(tǒng)中在內(nèi)存分配中的最小單位是 8 bytes。
如果想要添加的屬性不是像例子中這樣簡單的 enum,而是更加復(fù)雜的帶有多個(gè)屬性的類型的話,這一開銷會更大。
另外,如果我們還要添加其他屬性,很容易想到的方法是繼續(xù)在 TrafficLight 上加入更多的存儲屬性。這其實(shí)是很沒有擴(kuò)展性的方法,我們并不能在 extension 中添加存儲屬性:
// 無法編譯
extension TrafficLight {
enum A {
case a
}
var myOption: A = .a // Extensions must not contain stored properties
}
需要修改 TrafficLight 的源碼,才能添加這個(gè)選項(xiàng),而且還需要為添加的屬性設(shè)置合適的初始值,或者提供額外的 init 方法。如果我們不能直接修改 TrafficLight 的源碼 (比如這個(gè)類型是別人的代碼,或者是被封裝到 framework 里的),那么像這樣的添加選項(xiàng)的方式其實(shí)是無法實(shí)現(xiàn)的。
Option Pattern
可以用 Option Pattern 來解決這個(gè)問題。在 TrafficLight 中,我們不去提供專用的 preferredGreenLightColor,而是定義一個(gè)泛用的 options 字典,來將需要的選項(xiàng)值放到里面。為了限定能放進(jìn)字典中的值,新建一個(gè) TrafficLightOption 協(xié)議:
public protocol TrafficLightOption {
associatedtype Value
/// 默認(rèn)的選項(xiàng)值
static var defaultValue: Value { get }
}
在 TrafficLight 中,加入下面的 options 屬性和下標(biāo)方法:
public class TrafficLight {
// ...
// 1
private var options = [ObjectIdentifier: Any]()
public subscript<T: TrafficLightOption>(option type: T.Type) -> T.Value {
get {
// 2
options[ObjectIdentifier(type)] as? T.Value
?? type.defaultValue
}
set {
options[ObjectIdentifier(type)] = newValue
}
}
// ...
}
- 只有滿足
Hashable的類型,才能作為options字典的 key。ObjectIdentifier通過給定的類型或者是 class 實(shí)例,可以生成一個(gè)唯一代表該類型和實(shí)例的值。它非常適合用來當(dāng)作options的 key。 - 通過 key 在
options中尋找設(shè)置的值。如果沒有找到的話,返回默認(rèn)值type.defaultValue。
現(xiàn)在,對 TrafficLight.GreenLightColor 進(jìn)行擴(kuò)展,讓它滿足 TrafficLightOption。如果 TrafficLight 已經(jīng)被打包成 framework,我們甚至可以把這部分代碼從 TrafficLight 所在的 target 中拿出來:
extension TrafficLight {
public enum GreenLightColor: TrafficLightOption {
case green
case turquoise
public static let defaultValue: GreenLightColor = .green
}
}
我們將 defaultValue 聲明為了 GreenLightColor 類型,這樣TrafficLightOption.Value 的類型也將被編譯器推斷為 GreenLightColor。
最后,為這個(gè)選項(xiàng)提供 setter 和 getter:
extension TrafficLight {
public var preferredGreenLightColor: TrafficLight.GreenLightColor {
get { self[option: GreenLightColor.self] }
set { self[option: GreenLightColor.self] = newValue }
}
}
現(xiàn)在,你可以像之前那樣,通過直接在 light 上設(shè)置 preferredGreenLightColor 來使用這個(gè)選項(xiàng),而且它已經(jīng)不是 TrafficLight 的存儲屬性了。只要不進(jìn)行設(shè)置,它便不會帶來額外的開銷。
light.preferredGreenLightColor = .turquoise
有了 TrafficLightOption,現(xiàn)在想要為 TrafficLight 添加選項(xiàng)時(shí),就不需要對類型本身的代碼進(jìn)行改動(dòng)了,我們只需要聲明一個(gè)滿足 TrafficLightOption 的新類型,然后為它實(shí)現(xiàn)合適的計(jì)算屬性就可以了。這大幅增加了原來類型的可擴(kuò)展性。
總結(jié)
Option Pattern 是一種受到 SwiftUI 的啟發(fā)的模式,它幫助我們在不添加存儲屬性的前提下,提供了一種向已有類型中以類型安全的方式添加“存儲”的手段。
這種模式非常適合從外界對已有的類型進(jìn)行功能上的添加,或者是自下而上地對類型的使用方式進(jìn)行改造。這項(xiàng)技術(shù)可以對 Swift 開發(fā)和 API 設(shè)計(jì)的更新產(chǎn)生一定有益的影響。反過來,了解這種模式,相信對于理解 SwiftUI 中的很多概念,比如 PreferenceKey 和 alignmentGuide 等,也會有所助益。
以上就是Swift 中如何使用 Option Pattern 改善可選項(xiàng)的 API 設(shè)計(jì)的詳細(xì)內(nèi)容,更多關(guān)于Swift 改善api設(shè)計(jì)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mac git xcrun error active developer path 錯(cuò)誤
本文主要是講訴了如何解決在mac下使用git;xcode4.6的環(huán)境時(shí),出現(xiàn)了錯(cuò)誤(mac git xcrun error active developer path)的解決辦法,希望對大家有所幫助2014-09-09
Swift實(shí)現(xiàn)表格視圖單元格單選(2)
這篇文章主要為大家詳細(xì)介紹了Swift實(shí)現(xiàn)表格視圖單元格單選的第二篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
在一個(gè)項(xiàng)目中同時(shí)使用Swift和Objective-C代碼混合編程的方法
這篇文章主要介紹了在一個(gè)項(xiàng)目中同時(shí)使用Swift和Objective-C代碼的方法,在一個(gè)工程中同時(shí)使用Swift和Objective-C混合語言編程的方法,需要的朋友可以參考下2014-07-07
Swift3.0剪切板代碼拷貝及跨應(yīng)用粘貼實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Swift3.0剪切板代碼拷貝及跨應(yīng)用粘貼的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
詳談swift內(nèi)存管理中的引用計(jì)數(shù)
下面小編就為大家?guī)硪黄斦剆wift內(nèi)存管理中的引用計(jì)數(shù)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
Swift4.1轉(zhuǎn)場動(dòng)畫實(shí)現(xiàn)側(cè)滑抽屜效果
這篇文章主要為大家詳細(xì)介紹了Swift4.1轉(zhuǎn)場動(dòng)畫實(shí)現(xiàn)側(cè)滑抽屜效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06

