🕊
コールバック地獄が生まれる理由
非同期処理を順番に実行したい時に生まれやすい
プログラム処理には時間軸別で、同期処理と非同期処理の 2 パターンがあります。同期処理を複数実行したい場合は、上から順にメソッドを書いていくだけで、その順番でコードが実行されます。
ですが非同期処理の場合は、何も考えずに書くと、コールバックを多用したコードになってしまいます。
下のコードを見てみてください、
ある社員がランチを食べて、ランチにかかった時間を上司に報告するという一連の流れを実装してみます。
// 非同期処理をするメソッド
/// ランチを食べる(かかった時間を返す)
func eatLunch(user: User, @escaping finishEatingNotifier: ((Int) -> Void)?) {
let timeToEat: Int = Int.random(in: 1...5)
// sleepメソッドが非同期処理なので、
// eatLunchメソッドも非同期扱いになる
sleep(timeToEat)
finishEatingNotifier(timeToEat)
}
// 処理結果を受け取る側
/// ランチにかかった時間を上司に報告する
eatLunch(user: currentUser) { [weak self] result in
self?.report(result, from: currentUser, to: boss)
}
ランチにかかった時間を上司に報告した後、上司から指示された次の仕事に取り組むメソッドを増やしたくなりました。
そしてその仕事がうまくできたかどうかで、その後の処理を分けたくなりました。
この段階でどんな実装になるでしょうか。
/// ランチにかかった時間を上司に報告する
eatLunch(user: currentUser) { [weak self] result in
self?.report(result, from: currentUser, to: boss) { [weak self] nextTask in
// 報告した際に指示された、午後の分の仕事に取り掛かる(非同期)
self?.work(on: nextTask) { [weak self] result in
switch result {
case .success:
self?.takeBreak()
case .failure(let reason):
self?.askBoss(with: reason)
}
}
}
}
この地点ですでに見にくくなってきました。
このように非同期処理の直列実行の数が増えてくると、クロージャのネストが繰り返されてしまいます。
その結果クロージャ毎のスコープが分かりづらくなってしまい、コードの可読性・保守性・再利用性を下げます。
これがコールバック地獄と呼ばれる理由です。
どうやって回避するか
非同期処理の実装には、GCD, RxSwift, Combine, async/await 等、時代に合わせて様々な技術がありますが、技術の流れには共通して言えることがあります。
それは、
非同期処理も同期処理と同じように、処理の流れを直列に書けるようにする事で、ネストが深くなって生まれるコールバック地獄を避け、コードの可読性・保守性・再利用性を高めようとしている
という事です。
先ほど列挙した技術は大別すると、イベントを一度だけ監視できればよいか、それとも継続して監視する必要があるかで使い分けます。
具体的には、以下のように使い分けるのがおすすめです。
-
イベントを一度だけ監視したい
- 必ずメインスレッドで実行したい
- iOS 13 未満 → GCD (Grand Central Dispatch)
- iOS 13 以上 →
@MainActor
属性
- メインスレッドに限定しない
- iOS 15 未満
- iOS 15 以上 → async/await
- 必ずメインスレッドで実行したい
- イベントを継続して監視(=購読)したい
Discussion