こんにちは。
主にiOS関連のアプリ開発を担当しているよのでらです。
業務中に、複数の非同期処理を同期的に実行するための実装が必要になりました。
通常、非同期処理の結果を待ってから次を処理するように実装すると、ネストが深くなりがちで可読性やメンテナンス性が悪くなります。
そこで、それらの問題を解消するため非同期処理を同期的に繋げて実装可能なHydra/Promiseを導入してみました。
Hydra/Promiseとは
参考:Hydra
Hydra/Promiseは、非同期処理を便利に扱える処理のフレームワークです。
Promise自体は多くの言語にも存在しており、基本的な考え方は同じです。
そのため、今回は以下の事柄を踏まえて実装しました。
今回踏まえたこと
- 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(_data: Data) -> 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(Data()).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など便利な機能がありますが、今回はここまでとします。
最後までお読みいただき、ありがとうございました。