2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-13

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

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Proxyパターン概要

Proxy(プロキシ、代理人)とは、大まかに言えば、別の物のインタフェースとして機能するクラスである。
その「別の物」とは何でもよく、ネットワーク接続だったり、メモリ上の大きなオブジェクトだったり、複製がコスト高あるいは不可能な何らかのリソースなどである。

引用:Wikipedia

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

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

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 です。

// 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)

// 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.
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?