軟體設計

Swift 代理模式(Proxy Pattern):隱藏在 lazy 背後的代理物件

進行 iOS 開發時,常常會使用 lazy var 這一行。

閱讀 4 分鐘
Swift 代理模式(Proxy Pattern):隱藏在 lazy 背後的代理物件 封面圖

進行 iOS 開發時,常常會使用 lazy var 這一行。

我也是如此。需要延後重量級物件的初始化時,總是習慣性地加上它。

後來我突然好奇:lazy其實不就是設計模式教科書中的 Proxy Pattern 嗎?

先說結論。

lazy是語言內建版本的虛擬代理(Virtual Proxy),會代替物件的建立,直到真正需要該物件的時刻。

今天就按照我親自操作程式碼、逐步理解的順序,說明 Swift 的 Proxy Pattern,以及這個概念如何藏在lazy關鍵字後面。

先快速整理重點。

  1. 代理模式是在真正物件前放置代理人,以控制存取的結構型模式。
  2. lazy的目的,和延後建立物件的虛擬代理相同。
  3. 不過,lazy無法處理存取控制或記錄等功能,只負責延遲建立。
  4. 如果需要記錄或權限檢查,就必須自行建立代理物件。

Swift 的代理模式究竟是什麼?

Proxy 就是代理人的意思。

在負責實際工作的物件前,放置一個具有相同介面的代理人。

從外部看起來像是在呼叫真正的物件,但實際上會先經過代理人。

代理人可以直接轉送請求,也可以在中間追加處理,例如確認存取權限、記錄呼叫,或延後建立真正的物件。

常見用途主要有三種。

  • 虛擬代理:延後重量級物件的建立,直到實際使用時才建立
  • 保護代理:阻擋未授權的存取
  • 記錄代理:記錄呼叫內容

今天的主角是第一種,也就是虛擬代理。它正好和lazy相連。


隱藏在 lazy 背後的代理物件真面目

先想像一下處理高畫質影像的情境。

影像載入很耗費資源。畫面都還沒顯示,就預先全部載入會浪費記憶體。

因此有了「真正需要時再載入」的想法。這就是虛擬代理的核心。

先用代理模式直接實作,會像下面這樣。

protocol Image { func display() }

// 代理人:將真正物件的建立延後到需要時
final class ImageProxy: Image {
    private let filename: String
    private var real: RealImage?          // 尚未建立
    init(_ filename: String) { self.filename = filename }
    func display() {
        if real == nil { real = RealImage(filename) }  // 在此刻建立
        real?.display()
    }
}

只有第一次呼叫display()時,才會建立RealImage

在此之前,ImageProxy只帶著檔案名稱安靜等待。這正是代理人的工作。

不過 Swift 已將這個模式融入語法中,就是lazy

代理人代替真正物件被持有的瞬間
代理人代替真正物件被持有的瞬間
final class Gallery {
    // 第一次存取這個屬性時只建立一次
    lazy var cover: RealImage = RealImage("cover.png")
}

let g = Gallery()   // 尚未 RealImage 建立
g.cover.display()   // 在這裡才建立

建立Gallery()時,RealImage並不存在。

第一次接觸g.cover的瞬間才會建立。也就是由編譯器代替上方ImageProxy完成延遲建立。


那麼只要有 lazy,就不需要代理模式了嗎?

我一開始的疑問也是這個。

答案是「不是」。

lazy只能代替你完成延遲建立。

它無法阻擋存取、記錄呼叫,也無法根據條件回傳不同物件。

我把兩者的差異整理成表格。

項目 lazy 屬性 自行建立的代理
延遲建立 可以 可以
存取權限檢查 不可以 可以
呼叫記錄與快取 不可以 可以
程式碼量 一行 一個類別
可重複使用性 限於該屬性 可在多處重複使用

所以判斷標準很簡單。

如果純粹只要「延後建立」,lazy一行就夠了,沒必要特別建立類別。

但若需要確認權限、記錄,或包裝遠端呼叫等中介處理,就必須自行建立代理物件。

需要延後重量級初始化時,我總是先想到這一行
需要延後重量級初始化時,我總是先想到這一行

還有一點需要注意。

lazy不保證執行緒安全。

如果多個執行緒同時第一次存取,初始化可能會執行兩次。在多執行緒環境中,最好自行用代理包裝這部分並處理同步。


常見問題整理

Q. lazy 和計算屬性(computed property)有什麼不同?

lazy只會在第一次計算並儲存值。計算屬性則會在每次存取時重新計算。若希望重量級初始化只執行一次,應使用lazy

Q. lazy 為什麼只能宣告成 var?

因為初始化時間在之後,值確定前的執行個體會短暫存在。let不允許這種情況,所以lazy let在語法上不可能。

親自建立代理類別,就能立刻理解 lazy 代替你做了什麼
親自建立代理類別,就能立刻理解 lazy 代替你做了什麼

總結來說,lazy就像語言預先包裝好的禮物,將代理模式中的虛擬代理封裝起來。

一行就能完成的事,不必特別建立代理類別;但理解那一行背後的概念後,看程式碼的角度確實會改變。

從今天開始,使用lazy var時,不妨想著:「啊,我現在是在放置一個代理人。」你會覺得這個模式親近許多。

延伸閱讀