この記事シリーズは、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に書き込むケースで、
- 各項目の値の論理チェック行い、正当な値だった場合に
- 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.