1. 程式人生 > 程式設計 >Swift 中如何使用 Option Pattern 改善可選項的 API 設計

Swift 中如何使用 Option Pattern 改善可選項的 API 設計

SwiftUI 中提供了很多“新穎”的 API 設計思路和 Swift 的使用方式,我們可以進行借鑑,並反過來使用到普通的 Swift 程式碼中。PreferenceKey 的處理方式就是其中之一:它通過 protocol 的方式,為子 view 們提供了一套模式,讓它們能將自定義值以型別安全的方式,向上傳到父 view 去。如果有機會,我會再專門介紹 PreferenceKey,但這種設計的模式其實和 UI 無關,在一般的 Swift 裡,我們也能使用這種方法來改善 API 設計。

在這篇文章裡,我們就來看看要如何做。文中相關的程式碼可以在這裡找到。你可以將這些程式碼複製到 Playground 中執行並檢視結果。

紅綠燈

用一個交通訊號燈作為例子。

Swift 中如何使用 Option Pattern 改善可選項的 API 設計

作為 Model 型別的 TrafficLight 型別定義了 .stop.proceed .caution 三種 State,它們分別代表停止、通行和注意三種狀態 (當然,通俗來說就是“紅綠黃”,但是 Model 不應該和顏色,也就是 View 層級相關)。它還持有一個 state 來表示當前的狀態,並在設定時將這個狀態通過 onStateChanged 傳送出去:

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

其餘部分的邏輯和本次主題無關,不過它們也比較簡單。如果你有興趣的話,可以點開下面的詳情檢視。但這不影響本文的理解。

TrafficLight 的其他部分

在 (ViewController 中) 使用這個紅綠燈也很簡單。我們按照紅綠黃的顏色,在 onStateChanged 中設定 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 的變化而變更了:

Swift 中如何使用 Option Pattern 改善可選項的 API 設計

青色訊號

世界很大,有些地方 (比如日本) 會使用傾向於青色,或者實際上應該是綠鬆色 (turquoise),來表示“可以通行”。有時候這也是技術的限制或者進步所帶來的結果。

The green light was traditionally green in colour (hence its name) though modern LED green lights are turquoise.

– Wikipedia 中關於 Traffic light 的記述

Swift 中如何使用 Option Pattern 改善可選項的 API 設計

假設我們想要讓 TrafficLight 支援青色的綠燈,一個能想到的最簡單的方式,就是在 TrafficLight 裡為“綠燈顏色”提供一個選項:

public class TrafficLight {
  public enum GreenLightColor {
    case green
    case turquoise
  }
  public var preferredGreenLightColor: GreenLightColor = .green
  
  //...
}

然後在 ViewController 中使用對應的顏色:

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
}

這樣做當然能夠解決問題,但是也會帶來一些隱患。首先,需要在 TrafficLight 中新增一個額外的儲存屬性 preferredGreenLightColor,這使得 TrafficLight 示例所使用的記憶體開銷增加了。在上例中,額外的 GreenLightColor 屬性將會為每個例項帶來 8 byte 的開銷。 如果我們需要同時處理很多 TrafficLight 例項,而其中只有很少數需要 .turquoise 的話,這個開銷就非常可惜了。

嚴格來說,上例的 TrafficLight.GreenLightColor 列舉其實只需要佔用 1 byte。但是 64-bit 系統中在記憶體分配中的最小單位是 8 bytes。

如果想要新增的屬性不是像例子中這樣簡單的 enum,而是更加複雜的帶有多個屬性的型別的話,這一開銷會更大。

另外,如果我們還要新增其他屬性,很容易想到的方法是繼續在 TrafficLight 上加入更多的儲存屬性。這其實是很沒有擴充套件性的方法,我們並不能在 extension 中新增儲存屬性:

// 無法編譯
extension TrafficLight {
  enum A {
    case a
  }
  var myOption: A = .a // Extensions must not contain stored properties
}

需要修改 TrafficLight 的原始碼,才能新增這個選項,而且還需要為新增的屬性設定合適的初始值,或者提供額外的 init 方法。如果我們不能直接修改 TrafficLight 的原始碼 (比如這個型別是別人的程式碼,或者是被封裝到 framework 裡的),那麼像這樣的新增選項的方式其實是無法實現的。

Option Pattern

可以用 Option Pattern 來解決這個問題。在 TrafficLight 中,我們不去提供專用的 preferredGreenLightColor,而是定義一個泛用的 options 字典,來將需要的選項值放到裡面。為了限定能放進字典中的值,新建一個 TrafficLightOption 協議:

public protocol TrafficLightOption {
  associatedtype Value

  /// 預設的選項值
  static var defaultValue: Value { get }
}

TrafficLight 中,加入下面的 options 屬性和下標方法:

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
    }
  }
  
  // ...  
}
  1. 只有滿足 Hashable 的型別,才能作為 options 字典的 key。ObjectIdentifier 通過給定的型別或者是 class 例項,可以生成一個唯一代表該型別和例項的值。它非常適合用來當作 options 的 key。
  2. 通過 key 在 options 中尋找設定的值。如果沒有找到的話,返回預設值 type.defaultValue

現在,對 TrafficLight.GreenLightColor 進行擴充套件,讓它滿足 TrafficLightOption。如果 TrafficLight 已經被打包成 framework,我們甚至可以把這部分程式碼從 TrafficLight 所在的 target 中拿出來:

extension TrafficLight {
  public enum GreenLightColor: TrafficLightOption {
    case green
    case turquoise

    public static let defaultValue: GreenLightColor = .green
  }
}

我們將 defaultValue 宣告為了 GreenLightColor 型別,這樣TrafficLightOption.Value 的型別也將被編譯器推斷為 GreenLightColor

最後,為這個選項提供 setter 和 getter:

extension TrafficLight {
  public var preferredGreenLightColor: TrafficLight.GreenLightColor {
    get { self[option: GreenLightColor.self] }
    set { self[option: GreenLightColor.self] = newValue }
  }
}

現在,你可以像之前那樣,通過直接在 light 上設定 preferredGreenLightColor 來使用這個選項,而且它已經不是 TrafficLight 的儲存屬性了。只要不進行設定,它便不會帶來額外的開銷。

light.preferredGreenLightColor = .turquoise

有了 TrafficLightOption,現在想要為 TrafficLight 新增選項時,就不需要對型別本身的程式碼進行改動了,我們只需要宣告一個滿足 TrafficLightOption 的新型別,然後為它實現合適的計算屬性就可以了。這大幅增加了原來型別的可擴充套件性。

總結

Option Pattern 是一種受到 SwiftUI 的啟發的模式,它幫助我們在不新增儲存屬性的前提下,提供了一種向已有型別中以型別安全的方式新增“儲存”的手段。

這種模式非常適合從外界對已有的型別進行功能上的新增,或者是自下而上地對型別的使用方式進行改造。這項技術可以對 Swift 開發和 API 設計的更新產生一定有益的影響。反過來,瞭解這種模式,相信對於理解 SwiftUI 中的很多概念,比如 PreferenceKey alignmentGuide 等,也會有所助益。

以上就是Swift 中如何使用 Option Pattern 改善可選項的 API 設計的詳細內容,更多關於Swift 改善api設計的資料請關注我們其它相關文章!