Swift Hydra/Promiseで非同期処理を同期的に処理する

こんにちは。IT部のよのでらです。
主にSwiftでiOSアプリの開発を担当しています。

業務中の実装で、複数の非同期処理を同期的に実行する必要がありました。
通常、非同期処理の結果を待ってから次を処理するように実装すると、ネストが深くなりがちで可読性やメンテナンス性が悪くなります。
そこで、それらの問題を解消するため非同期処理を同期的に繋げて実装可能なHydra/Promiseを導入してみました。

Hydra/Promiseとは

参考 Hydra

Hydraは、非同期処理を便利に扱える処理のフレームワークです。
Promise自体は多くの言語にも存在しており、基本的な考え方は同じです。
そのため、今回は以下の事柄を踏まえて実装しました。

今回踏まえたこと

  • 非同期の処理をわかりやすく制御するもの(今回は非同期を同期的に制御)
  • resolve(解決)、reject(拒否)、pending(保留中)の状態があり
  • resolveは次の処理(then)の実行を許可
  • rejectは次の処理の実行を拒否(catchへ)
  • pendingはまだ未処理

今回の導入でのメリット

  • 非同期処理が同期処理のように順番に実行されるため処理の流れを把握しやすい(メソッドチェーン)
  • ネストが浅くなるため可読性が高くメンテナスしやすい(処理の追加や入れ替えがあっても楽)
  • どのスレッドで動作させるかを管理しやすい

今回の導入でのデメリット

多少の学習コストが必要です。
はじめは、メソッドチェーンや値の受け渡しがうまく動かずに苦労しました。
試行錯誤の末、以下の理解で少しずつ実装できるようになりました。

例えば、処理結果のデータ型がDataの場合は、

  • メソッドの型はPromise<Data>
  • メソッドが正常終了の場合はresolve(Data)を実行
  • メソッドが異常終了の場合はreject(Error)を実行
  • メソッドの処理結果が正常の場合は直後のthenで受け取る
  • メソッドの処理結果が異常の場合はcatchで受け取る

Hydra/Promiseの使い方

5つの非同期処理を順に実行し、全て成功して正常終了、途中で失敗したら異常終了にしたいとします。 その際の実装サンプルを、以下の2つで示します。

その1、コールバックでの実装例

API1クラスのmethod1からmethod5をバックグランドで順番に処理すると仮定します。
メイン処理のネストが深いですね。
これはサンプルなのでシンプルに見えますが、実際は更に細かい処理があるため、もっと複雑になります。
これが導入を考え始めた動機です。

class API1 {
    func method1(success: ((Data) -> Void)? = nil, failure: ((_ error: Error) -> Void)? = nil) {
        // 成功
        success?(Data())
        // 失敗
        // failure(error)
    }

    func method2(_ data: Data, success: ((Data) -> Void)? = nil, failure: ((_ error: Error) -> Void)? = nil) {
        // 成功
        success?(Data())
        // 失敗
        // failure(error)
    }

    func method3(_ data: Data, success: ((Data) -> Void)? = nil, failure: ((_ error: Error) -> Void)? = nil) {
        // 成功
        success?(Data())
        // 失敗
        // failure(error)
    }

    func method4(_ data: Data, success: ((Data) -> Void)? = nil, failure: ((_ error: Error) -> Void)? = nil) {
        // 成功
        success?(Data())
        // 失敗
        // failure(error)
    }

    func method5(_ data: Data, success: ((Data) -> Void)? = nil, failure: ((_ error: Error) -> Void)? = nil) {
        // 成功
        success?(Data())
        // 失敗
        // failure(error)
    }
}

/// メイン処理
DispatchQueue.global().async {
    API1().method1(success: { data in
        API1().method2(data, success: { data in
            API1().method3(data, success: { data in
                API1().method4(data, success: { data in
                    API1().method5(data, success: { data in
                        print("Complete: \(data)");     // ネストがかなり深い
                    }, failure: { error in
                        print("Error: \(error)");
                    })
                }, failure: { error in
                    print("Error: \(error)");
                })
            }, failure: { error in
                print("Error: \(error)");
            })
        }, failure: { error in
            print("Error: \(error)");
        })
    }, failure: { error in
        print("Error: \(error)");
    })
}

その2、Hydra/Promiseでの実装例

上記をAPI2クラスとして、Hydra/Promiseで書き直してみます。
メソッド部分は少し複雑になりましたが、メイン処理はかなりスッキリしました。

class API2 {
    func method1() -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }

    func method2(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }

    func method3(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }

    func method4(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }

    func method5(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }
}

// メイン処理
API2().method1().then {data in
    API2().method2(data)
}.then { data in
    API2().method3(data)
}.then { data in
    API2().method4(data)
}.then { data in
    API2().method5(data)
}.then { data in
    print("Complete!!!: \(data)");
}.catch { error in
    print("Error: \(error)"); // エラーが1箇所で処理できる
}

注意点

あまり長いメソッドチェーンはできないようです(少し残念)。
上記のように、式の分割を促すコンパイラーエラーが出ます。

そのため、私は以下のように分割して使っています。

// 10個のメソッドを持つクラス
class API3 {
    func method1() -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }
    func method2(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }
    // 〜省略〜
    func method8(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }
    func method9(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }
    func method10(_ data: Data) -> Promise<Data> {
        return Promise<Data>(in: .background, { resolve, reject, _ in
            // 成功
            resolve(Data())
            // 失敗
            // reject(error)
        })
    }

}

// 10個のメソッドの実行を試みるも...
func group1() -> Promise<Data> {
    return Promise<Data>(in: .background, { resolve, reject, _ in
        API3().method1().then { data in
            API3().method2(data)
        }.then { data in
            API3().method3(data)
        }.then { data in
            API3().method4(data)
        }.then { data in
            API3().method5(data)
        }.then { data in
            API3().method6(data)
        }.then { data in
            API3().method7(data)
        }.then { data in
            API3().method8(data)
        }.then { data in
//            API3().method9(data) // これ以上はコンパイラーエラーになるためグループ2へ分割する
//        }.then { data in
            resolve(data)
        }.catch { error in
            reject(error)
        }
    })
}

// 分割した残りのメソッドを実行
func group2(_ data: Data) -> Promise<Data> {
    return Promise<Data>(in: .background, { resolve, reject, _ in
        API3().method9(data).then { data in
            API3().method10(data)
        }.then { data in
            resolve(data)
        }.catch { error in
            reject(error)
        }
    })
}

group1().then { data in
    group2(data)
}.then { data in
    print(data)
}.catch { error in
    print(error)
}.always {
    print("終わったよ")
}

最後に

今回使用したthen、catchの他にも、alwaysやretryなど便利な機能がありますが、今回はここまで。
最後までお読みいただき、ありがとうございました。

よのでら
CSVIT事業部 よのでら
便利な機能と新しい機能には目がないです。