ソフトウェア設計

SwiftのProxy Pattern(プロキシパターン)、lazyに隠れた代理オブジェクト

iOS開発では、lazy varを実によく使うことになります。

読了 6 分
SwiftのProxy Pattern(プロキシパターン)、lazyに隠れた代理オブジェクトのカバー画像

iOS開発では、lazy varを実によく使うことになります。

私もそうでした。重いオブジェクトの初期化を後回しにするとき、習慣のように付けていました。

ふと疑問に思いました。lazyとは、実はデザインパターンの教科書に出てくるProxy Patternと同じではないのか、と。

まず結論からお話しします。

lazyは、実際のオブジェクトが必要になるまで生成を肩代わりして遅延させるVirtual Proxyを、言語に組み込んだものです。

今日はSwiftのProxy Patternとは何か、そしてその概念がlazyキーワードの裏側にどう隠れているのか、実際にコードを触って理解した順に説明します。

まずは要点をまとめます。

  1. Proxy Patternは、実際のオブジェクトの前に代理人を置いてアクセスを制御する構造パターンです。
  2. lazyは、その中でも生成を遅延させるVirtual Proxyと同じ目的を持ちます。
  3. ただし、lazyができるのは遅延生成だけで、アクセス制御やロギングには対応しません。
  4. ロギングや権限チェックが必要なら、Proxyオブジェクトを自分で作る必要があります。

SwiftのProxy Patternとは一体何でしょうか?

Proxyは日本語で代理人です。

実際に仕事をするオブジェクトの前に、同じインターフェースを持つ代理人を置きます。

外部からは実際のオブジェクトを呼んでいるように見えますが、実際には代理人を経由します。

代理人はリクエストをそのまま転送することもあれば、権限確認や呼び出しの記録、実際のオブジェクト生成の遅延など、途中で処理を追加することもあります。

代表的な用途は3つです。

  • Virtual Proxy:重いオブジェクトの生成を実際に使うまで遅延
  • Protection Proxy:権限のないアクセスを防止
  • Logging Proxy:呼び出し履歴を記録

今日の主役は1つ目のVirtual Proxyです。lazyとまさに結び付く部分だからです。


lazyに隠れた代理オブジェクトの正体

高解像度画像を扱う場面を考えてみましょう。

画像の読み込みは重い処理です。画面に表示されていないのに先にすべて読み込むと、メモリの無駄になります。

そこで「本当に必要なときに読み込もう」という発想が生まれます。これがVirtual Proxyの核心です。

まずProxy Patternで直接実装すると、次のようになります。

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 {
    // このプロパティに初めてアクセスしたとき、1度だけ生成される
    lazy var cover: RealImage = RealImage("cover.png")
}

let g = Gallery()   // まだ RealImage 生成されていない
g.cover.display()   // ここで初めて生成

Gallery()を作成した時点では、RealImageは存在しません。

g.coverに初めて触れた瞬間に生成されます。上のImageProxyが行っていた遅延生成を、コンパイラが代わりに作ってくれるわけです。


では、lazyがあればProxy Patternは必要ないのでしょうか?

私が最初に抱いた疑問もこれでした。

答えは「いいえ」です。

lazyが代わりに行うのは、遅延生成だけです。

アクセスを遮断したり、呼び出しを記録したり、条件に応じて別のオブジェクトを返したりはできません。

違いを表にまとめました。

項目 lazyプロパティ 自作Proxy
遅延生成 可能 可能
アクセス権限チェック 不可 可能
呼び出しのロギング・キャッシュ 不可 可能
コード量 1行 1クラス
再利用性 そのプロパティに限定 複数箇所で再利用

基準はシンプルです。

純粋に「生成を遅らせればよい」だけなら、lazy1行が正解です。わざわざクラスを作る必要はありません。

一方、権限確認やロギング、リモート呼び出しのラップなど、途中で処理を挟むならProxyオブジェクトを自分で作る必要があります。

重い初期化を遅らせるとき、私はいつもまずこの1行を思い浮かべます
重い初期化を遅らせるとき、私はいつもまずこの1行を思い浮かべます

注意点が1つあります。

lazyはスレッドセーフを保証しません。

複数のスレッドが同時に初めてアクセスすると、初期化が2回発生する可能性があります。マルチスレッド環境では、ここを自作Proxyで包んで同期を自分で行うほうが安全です。


よくある質問のまとめ

Q. lazyとcomputed propertyは何が違いますか?

lazyは最初の1回だけ計算して値を保存します。computed propertyはアクセスするたびに再計算されます。重い初期化を1回だけ行いたいなら、lazyが適しています。

Q. lazyはなぜvarでしか宣言できないのですか?

初期化の時点が後になるため、値が決まる前のインスタンスが一時的に存在するからです。letはそれを許さないので、lazy letは構文上不可能です。

代理人クラスを自分で作ってみると、lazyが何を代わりにしているのかがよく分かります
代理人クラスを自分で作ってみると、lazyが何を代わりにしているのかがよく分かります

まとめると、lazyはProxy PatternのVirtual Proxyを言語があらかじめ包んでくれた、贈り物のような存在です。

1行で済むことのために代理人クラスを作る必要はありませんが、その1行の裏にある概念を知って使えば、コードを見る目が確実に変わります。

今日からlazy varを使うときは、「ああ、今代理人を1つ置いているんだ」と思い浮かべてみてください。パターンがずっと身近に感じられるはずです。

あわせて読みたい