[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ～Proxy～

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
**ごく普通のiOSアプリ開発でよくある状況**や
**Swiftのコアライブラリやフレームワークで使われているパターン**に
着目してデザインパターンを学び直してみた記録です。

**関連記事一覧**
[[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン](https://qiita.com/y-some/items/96db5f24ded4a1fab48f)

# Proxyパターン概要

> Proxy（プロキシ、代理人）とは、大まかに言えば、別の物のインタフェースとして機能するクラスである。
> その「別の物」とは何でもよく、ネットワーク接続だったり、メモリ上の大きなオブジェクトだったり、複製がコスト高あるいは不可能な何らかのリソースなどである。
 
引用：[Wikipedia](https://ja.wikipedia.org/wiki/Proxy_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)

さらに細分化した呼称および概要

**1. Virtual Proxy**
・コストのかかるオブジェクトの生成を代理して、メモリ使用量を削減したり処理時間を短縮します。
・Flyweightパターンを加えることが多いと思われます。Flyweightについては別記事をご参照ください。
[[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ～Flyweight～](https://qiita.com/y-some/items/6d00928a22b188ef64a3)

**2. Protection Proxy**
・オブジェクトごとに異なるアクセス権が必要なとき、アクセス制御を代理します。

**3. Remote Proxy**
・別ロケーションや別アドレス空間にあるオブジェクトのローカルな代理を提供します。

# 使い所

## Virtual Proxy / Remote Proxy

**『サーバー上の画像をアプリ上で表示する時、一度取得した画像はアプリ内にキャッシュしておき、毎回サーバーにアクセスすることを避ける』**というような例です。

・「メモリ使用量を削減したり処理時間を短縮する」という観点でみるとVirtual Proxy
・「別ロケーションや別アドレス空間にあるオブジェクトのローカルな代理を提供する」という観点でみるとRemote Proxy

## Protection Proxy

iOSアプリのアクセス制御となると大抵はUIを伴うので、アクセス制御に適用というのは個人的にピンと来ないです…

少しひねって考えると、

APIのレスポンスをローカルDBに書き込むケースで、
1. 各項目の値の論理チェック行い、正当な値だった場合に
2. insert/updateを行う
という流れが良くあると思います。

このような時、**論理チェックを別クラスに代理させるというパターン**はProtection Proxyとして考えられるのかも知れません。
（とはいえ構造が複雑になると思うので、そのような設計を選択すべきケースは少ないかも知れませんが）

# サンプルコード (Virtual Proxy)

Swiftバージョンは 5.1 です。

```swift
// Protocol
protocol ImageProvider {
    func requestImage(with urlString: String, completion: @escaping (UIImage?) -> Void)
}

// サーバーからImageを取得するクラス
class ImageRequest: ImageProvider {
    // サーバーからImageを取得する
    func requestImage(with urlString: String, completion: @escaping (UIImage?) -> Void) {
        guard let url = URL(string: urlString) else { return }
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            // エラー判定省略
            guard let data = data, let image = UIImage(data: data) else {
                print("Couldn't parse image.")
                completion(nil)
                return
            }
            completion(image)
        }.resume()
    }
}

// ローカルにImageをキャッシュするクラス
class ImageCache {
    // 画像キャッシュ
    static var imageCache = NSCache<AnyObject, AnyObject>()

    // キャッシュからImageを取得する
    class func searchImage(with urlString: String) -> UIImage? {
        return imageCache.object(forKey: urlString as AnyObject) as? UIImage
    }
    
    // キャッシュにImageを保存する
    class func saveImage(_ image: UIImage, for urlString: String) {
        imageCache.setObject(image, forKey: urlString as AnyObject)
    }
}

// Proxyクラス
class ImageProxy: ImageProvider {
    // Imageを（代理で）取得する
    func requestImage(with urlString: String, completion: @escaping (UIImage?) -> Void) {
        if let cacheImage = ImageCache.searchImage(with: urlString) {
            // キャッシュに存在する場合はキャッシュから取得する
            completion(cacheImage)
            return
        } else {
            // キャッシュに存在しない場合はサーバーから取得する
            ImageRequest().requestImage(with: urlString) { (image) in
                guard let image = image else {
                    completion(nil)
                    return
                }
                // 画像を取得できたらキャッシュに保存する
                ImageCache.saveImage(image, for: urlString)
                completion(image)
            }
        }
    }
}

// Usage
class ViewController: UIViewController {
// （省略）
    @IBAction func buttonTapped(_ sender: Any) {
        let imageView = UIImageView()
        let urlString = "https://upload.wikimedia.org/wikipedia/commons/5/56/Donald_Trump_official_portrait.jpg"
        ImageProxy().requestImage(with: urlString) { (image) in
            DispatchQueue.main.async {
                imageView.image = image
            }
        }
    }
}
```

# サンプルコード (Protection Proxy)

```swift
// Protocol
protocol ModelUpdate {
    func write(with name: String)
}

// Subject
class Model: ModelUpdate {
    let id: Int
    var name = ""

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

    func write(with name: String) {
        self.name = name
        print("Model was writed.")
    }
}

// Proxy
class ProxyModel: ModelUpdate {
    private let model: Model
    
    init(id: Int, name: String) {
        self.model = Model(id: id, name: name)
    }
    
    func write(with name: String) {
        if name.isEmpty {
            print("Name is empty.")
            return
        }
        if name.contains(where: { !$0.isASCII }) {
            print("Name is incorrect.")
            return
        }
        model.write(with: name)
    }
}

// Usage
let model = ProxyModel(id: 100, name: "Taro")
model.write(with: "")   // Name is empty.
model.write(with: "😃") // Name is incorrect.
model.write(with: "Hanako") // Model was writed.
```