Help us understand the problem. What is going on with this article?

PromisePatternをSwiftで実装してみた

More than 1 year has passed since last update.

デザインパターンをSwiftで書いてみます。
今回は、Promiseパターンを実装してみます。

description

  [定義]  (非同期処理も) 同期的に順番に実行する
   
  [用例]  2つのAPIを決めた順序で呼べる (始めの結果を 次の引数に使う とか)
    
  [備考]  順番を保証することからPromiseと呼ばれるようになったとか
      .then().then()と関数でチェーンする この順番の保証が
      Promiseの由来と思われる

sample code

例えば、通信周りで 非同期のWebAPIを連続して呼びたい時

なるべくシンプルな作りになるように意識してみましたので、
各フィードバック(成功時、失敗時)は、自分で記載しないと動きません。
ゆえにシンプル、ゆえに予想外の動きは起きにくい、、、かな

ViewController_or_Presenter_and_so_on
// MARK: - ApiFetch
extension SomethinghViewController: ApiFetch {

    func fetch() {

        let deferred = Deferred()
            deferred.then { (obj) in
                self.presenter.fetch({ (result) in
                    switch result {
                    case .success(let value): deferred.resolve(value)
                    case .failure(let error): deferred.reject(error)
                    }
                })
            }
            .then{ (obj) in
                self.presenter.fetchAnother{ (result) in
                    switch result {
                    case .success(let value): deferred.resolve(value)
                    case .failure(let error): deferred.reject(error)
                    }
                }
            }
            .fail { (err) in
                if let error = err as? NSError {
                    self.fetchFailed(error)
                }
                deferred.finish()
            }
            .finally { (_) in
                self.fetched()
            }
            .fire()
    }

    func fetchFailed(_ error: NSError) {
        // write if need
    }

    func fetchSucceeded() {
        // write if need
    }

    func fetched() {
        // write if need
    }

}

ついでですが、protocolでコールバックで呼ばれるメソッドを定義しておくと
各使用箇所で統一されていいよね。(プロトコル指向を意識!)

ApiFetch
protocol ApiFetch {

    func fetch()

    func fetchFailed(_ error: NSError)

    func fetchSucceeded()

    func fetched()

}

Promiseパターンを実装したDeferredクラスはこちら
いろんな方がこのパターンを実装していますので大分参考にさせて頂きました。

Deferred.swift
class Deferred: NSObject {

    // MARK: - properties
    enum State: Int{
        case unresolved
        case resolved
        case rejected
    }
    fileprivate var state: State = .unresolved
    fileprivate var resolvedTasks:  Array<(Any?)->Void> = []
    fileprivate var failedTasks:    Array<(Any?)->Void> = []  // rejecteds
    fileprivate var finallyTasks:   Array<(Any?)->Void> = []
    fileprivate var responseObject: Any? = nil


    // MARK: - public
    /// stack
    /// Feedback is necessary to advance the task.
    func then(_ callBack: @escaping (Any)->Void) -> Deferred {
        resolvedTasks.append(callBack)
        return self
    }
    /// stack
    /// Feedback is necessary to advance the task.
    func fail(_ callBack: @escaping (Any)->Void) -> Deferred {
        failedTasks.append(callBack)
        return self
    }
    /// stack
    func finally(_ callBack: @escaping (Any)->Void) -> Deferred {
        finallyTasks.append(callBack)
        return self
    }
    /// trigger
    func fire() {
        switch state {
        case .unresolved:
            self.switchResolved()
            self.excute()
        case .resolved, .rejected:
            break // Do nothing
        }
    }
    /// feedback
    func resolve(_ response: Any? = nil){
        switch state {
        case .unresolved, .resolved:
            self.switchResolved()
            self.responseObject = response
            self.excute()
        case .rejected:
            break // Do nothing
        }
    }
    /// feedback
    func reject(_ response: Any? = nil){
        // do any state
        self.switchRejected()
        self.responseObject = response
        self.excute()
    }
    /// feedback
    func finish(){
        self.excute()
    }

}



// MARK: - private
extension Deferred {

    fileprivate func excute() {
        switch state {
        case .unresolved, .resolved:
            self.executeResolveTask()
        case .rejected:
            self.execureFailureTask()
        }
    }

    fileprivate func switchResolved() {
        state = .resolved
    }

    fileprivate func switchRejected() {
        state = .rejected
    }

    fileprivate func executeResolveTask() {
        if let first = resolvedTasks.first {
            resolvedTasks.removeFirst(1)
            let response = self.responseObject ?? nil
            self.responseObject = nil
            first(response)
        }
        else if let final = finallyTasks.first {
            finallyTasks.removeFirst(1)
            final(nil)
        }
    }

    fileprivate func execureFailureTask() {
        if let first = failedTasks.first {
            failedTasks.removeFirst(1)
            let response = self.responseObject ?? nil
            self.responseObject = nil
            first(response)
        }
        else if let final = finallyTasks.first {
            finallyTasks.removeFirst(1)
            final(nil)
        }
    }

}

Impressions

使ってみて、結構非同期処理など安心安定した感があります。
実装がパターン化できて、アーキテクチャーとして纏まり、
一貫性のあるコードになってきた気がします。(イイですね。)

世の中的にPromiseパターンはしっかり使われていることに気がつきました。
・RxSwiftのストリーム
・ReactorKit(RxSwift)
 ...RxSwiftのストリームって、要はPromiseだったんだ、、

Webの世界でも使われているんですね
・ECMAScript6.0(?)のPromiseメソッド
・(↑JSでPromiseということは、)node.js

APIと連携など非同期の連続処理が、
きっとどのプラットフォームでも必要なんですね。
みなさんは、こういう時どうしているのでしょうか?

今回のコードはこちらになります。
https://github.com/YuuichiWatanabe/SimpleDeferred

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away