プロキシーパターンをSwift5で実装する


※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。

The Proxy(プロクシ)

0. プロクシの意義

ある特定のオブジェクトに直接アクセスさせず、間接的にアクセスするようにするパターンをプロクシパターンと言う(Proxyは代理というような意味)。

具体的には、
・ バーチャルプロクシ
・ リモートプロクシ
・ プロテクティブプロクシ
の三種がある。

注意点は、プロクシを経由せずに直接目的のオブジェクトにアクセスできるような抜け道を用意してはならないという事である。それではプロクシパターンの意味がなくなってしまう。

1. Virtual Proxy(バーチャルプロクシ)

オブジェクト生成にコストがかかる場合、その生成のタイミングを本当にオブジェクトが必要になるまで遅らせるパターンの事を言う。

Swiftでは、変数の前にlazy修飾詞をつける事で比較的簡単に実現できる。

ImageProxy.swift
public protocol RemoteImage: CustomStringConvertible {
    init(url: URL)
    var image: UIImage? {get}
    var url: URL {get}
    var hasContent: Bool {get}
}

extension RemoteImage {
    public var description: String {
        let description = self.hasContent ? "Image available. Retrieved from \(self.url.absoluteString)" : "No image available yet!"
        return description
    }
}

public class ImageProxy: RemoteImage {
    public required init(url: URL) {
        self.url = url
    }

    //lazy修飾詞をつける
    public lazy var image: UIImage? = { [unowned self] in
        var result: UIImage?
        if let img = try? UIImage(data:
            Data(contentsOf: self.url)
            ) {
            result = img
            self.hasContent = true
        }

        return result
    }()

    public let url: URL
    public var hasContent: Bool = false
}

プロクシを実際に利用してみると以下のようになる。

VirtualImageProxy.playground
guard let imageURL = URL(string: "https://developer.apple.com/swift/images/swift-og.png") else {
    fatalError("Could not create URL")
}

let imageProxy = ImageProxy(url: imageURL)

print(imageProxy)// No image available yet!

let image = imageProxy.image
print(imageProxy)//Image available. Retrieved from https://developer.apple.com/swift/images/swift-og.png

プロクシを生成した段階ではimageプロパティは実際に生成されておらず、実際にimageプロパティにアクセスした時初めて生成されている事がわかる。

この例のように、ネットワークを介してデータをダウンロードして画像を生成すると言う重い処理がある場面では、最初にオブジェクトを生成するのでなく実際にアクセスした段階で生成する事でリソースの節約につながる。

2. Remote Proxy(リモートプロクシ)

リモートプロクシは、ネットワーク接続などコストのかかる処理を実際に必要になるタイミングまで延期するプロクシパターンを言う。

具体的には、ネットワーク接続に必要な情報(URL,クロージャなど)を渡す処理と、実際にネットワーク接続を行う処理を分離し、後者を前者とは別のタイミングで行えるようにする。

RemoteDataProxy.swift
import Foundation

public protocol RemoteData {
    func data(url: URL, completionHandler: @escaping(Error?, Data?) -> Void) -> RemoteData

    func run()
}

public class RemoteDataProxy: RemoteData {
    fileprivate var callback: ((Error?, Data?) -> Void)?
    fileprivate var url: URL?
    public init() {}

    //URL,コンプリーションハンドラを渡す処理
    public func data(url: URL, completionHandler: @escaping(Error?, Data?) -> Void) -> RemoteData {
        self.url = url
        self.callback = completionHandler
        return self
    }

    //実際にネットワーク接続を行う処理
    public func run() {
        if let callback = self.callback,
            let url = self.url {
            URLSession.shared.dataTask(with: url) {(data, response, error) in
                guard let data = data, error == nil else {
                    print("Could not download data from URL \(url.absoluteString). Reason: \(error!.localizedDescription)")
                    callback(error, nil)
                    return
                }

                print("Data successfully fetched from URL \(url.absoluteString)")
                callback(nil, data)
            }.resume()

            print("Downloading data from URL \(url.absoluteString)")
        } else {
            print("run() called before invoking data(url: completionHandler:)")
        }
    }
}

そして下記のようにdata(func:completionHandler:)メソッドをrun()メソッドを別のタイミングで呼ぶことで、コストのかかるネットワーク接続処理を自由なタイミングまで延期できる。もし実際のネットワーク接続処理が必要なくなったら行わなくて済むことになるため、やはりリソースの削減につながる。

RemoteProxy.playground
import Foundation
import PlaygroundSupport

guard let dataURL = URL(string: "https://developer.apple.com/swift/images/swift-og.png") else {
    fatalError("Could not create URL")
}

//URL、クロージャなど接続処理に必要な情報を渡す。この時点では接続は行われない
let dataProxy = RemoteDataProxy().data(url: dataURL) {(error, data) in
    guard error == nil else {
        print("Could not retrieve data from URL \(dataURL.absoluteString)")
        return
    }

    print("\(data?.count ?? 0) bytes retrieved from URL \(dataURL.absoluteString)")
}

//Playgroundで非同期処理を許可する
PlaygroundPage.current.needsIndefiniteExecution = true

//延期されたネットワーク接続処理
dataProxy.run()

3. Protective Proxy(プロテクティブプロクシ)

個人情報などセンシティブな情報にアクセスさせる際、権限が無い者に見られては困るため、必ず認証を経てから行いたい場合がある。

このように目的のオブジェクトへのアクセスを制限し、認証を経てからでないとできないようにするパターンをプロテクティブプロクシという。

先に実装したImageProxyクラスを利用する形で、さらに認証機能を追加したSecureImageProxyクラスを実装する。

認証機能は、新しく作成したAuthenticatorクラスを利用する。

Authenticator.swift
public protocol Authenticating {
    var isAuthenticated: Bool {get}
    func authenticate(user: String) -> Bool
}

public class Authenticator: Authenticating {
    static public let shared = Authenticator()

    //認証が行われたか否かを表すBool型変数
    public var isAuthenticated: Bool = false

    //接続が許可されているユーザー名の一覧
    fileprivate let userWhiteList = ["John", "Mary", "Steve"]

    fileprivate let syncQueue = DispatchQueue(label: "com.leakka.authQueue")

    fileprivate init() {}

    //許可されたユーザーか否かを確認するメソッド
    public func authenticate(user: String) -> Bool {
        var result = false
        self.syncQueue.sync {
            result = self.userWhiteList.contains(user) ? true : false

            if result {
                print("Authorized!")
                self.isAuthenticated = true
            } else {
                print("Error: Unauthorized!")
                self.isAuthenticated = false
            }
        }
        return result
    }
}

さらにプロキシクラスは以下のようになる。

ImageProxy.swift

//private修飾詞に変更
private class ImageProxy: RemoteImage {
//中略
}

ImageProxyクラスの公開範囲をprivateに変更している。

これは、後述のSecureImageProxyでなく直接ImageProxyを使用し、認証を回避するというような事態を避けるためである。

ImageProxy.swift

//認証用のプロクシクラスを追加
public class SecureImageProxy: RemoteImage {

    //認証が完了していれば画像を返す
    public var image: UIImage? {
        get {
            return Authenticator.shared.isAuthenticated ? self.imageProxy.image : nil
        }
    }

    public let url: URL
    public var hasContent: Bool = false

    //ImageProxyクラスをprivateで保持
    fileprivate lazy var imageProxy: ImageProxy = ImageProxy(url: self.url)

    public required init(url: URL) {
        self.url = url
    }
}

実際に使用してみると、以下のようになる。

VirtualImageProxy.playground
import Foundation
import PlaygroundSupport

guard let imageURL = URL(string: "https://developer.apple.com/swift/images/swift-og.png") else {
    fatalError("Could not create URL")
}

let secureImageProxy = SecureImageProxy(url: imageURL)

print(secureImageProxy)// No image available yet!

Authenticator.shared.authenticate(user: "Jim")//Error: Unauthorized!
if secureImageProxy.image != nil {
    print("Proxy has a valid image.")
}

Authenticator.shared.authenticate(user: "John")//Authorized!
if secureImageProxy.image != nil {
    print("Proxy has a valid image.")//Proxy has a valid image.
}

PlaygroundPage.current.needsIndefiniteExecution = true

誤ったユーザー名では認証に失敗し画像にアクセスできない。

正しいユーザー名で認証に成功した後のみ、画像にアクセスできていることがわかる。

参考文献: https://www.amazon.com/Design-Patterns-Swift-implement-Improve-ebook/dp/B07MDD3FQJ