はじめに
今回は、スパゲッティコードにならないような順次処理を考えてみたので記事にしてみました。
特定の順番で毎回実行される処理って、条件分岐や繰り返しが無いので、シンプルに書くことができます。
しかし、非同期で実行される処理がある場合などは、可読性を意識して書かないと、案外メチャクチャになりがちです。(←過去の経験より)
この記事で学べること
- 順次処理とは?
- 非同期で実行される順次処理を実装する上での課題はなにか?
- 処理フローを追いやすい順次処理の書き方
結論だけ確認したい方は、「処理フローを追いやすい順次処理の書き方」
から読んでいただければと思います。
順次処理ってなに?
指定した順番に沿って順番に実行されていくプログラムのこと
日常生活にたとえてみると、
朝起きる → 朝食を食べる → 歯を磨く → 着替える → 家を出る
というような、一連の流れがこれに当てはまります。
とてもとてもシンプルな構造ですので、一見プログラムもシンプルになると思われがちです。
非同期処理を含む順次処理の課題とは?
さっきの日常生活のたとえを、実際の要件にありそうな仕様に変えてみます。
今回は、起動時の処理を例にして説明しようと思います。
アプリ起動 → バージョンチェックAPI呼出 → ユーザー情報取得API呼出 → 商品情報取得API呼出 → トップ画面へ遷移
この一連の流れをSwiftでコードに落とし込むとこんな感じになります。
今回の場合、アプリを起動すると、SplashViewController
が一番最初に呼ばれるとします。
※サンプルコードなので中身ちゃんと見なくて大丈夫です。
※非同期処理の[weak self]など割愛してます。
import UIKit
class SplashViewController: UIViewController {
let api: APIManeger = APIManeger()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1. 起動したのでバージョンチェック
fetchVersionInfo()
}
func fetchVersionInfo() {
print("!!!! start version check")
api.requestVersionCheck() { result in
if result.isSuccess {
// バージョンアップが必要ならAppStoreに飛ばす etc...
// バージョンチェック正常終了
// 2. ユーザー情報取得API呼出
self.fetchUserInfo()
} else {
// エラーだったらアプリ終了
}
}
}
func fetchUserInfo() {
print("!!!! start userInfo request")
api.requestUserInfo { result in
if result.isSuccess {
// ユーザー情報から必要な情報を取得したり、データを加工したりする処理 etc...
// 正常終了
// 3. 商品情報取得API呼出
self.fetchProductInfo()
} else {
// エラーだったらアプリ終了
}
}
}
func fetchProductInfo() {
print("!!!! start productInfo request")
api.requestProductInfo { result in
if result.isSuccess {
// 商品情報情報から必要な情報を取得したり、データを加工したりする処理 etc...
// 正常終了
// 全部終わったのでトップ画面へ遷移する
} else {
// エラーだったらアプリ終了
}
}
}
}
いかがでしょうか。
自分はこの実装を見たときに
「順番で実行されるメソッドがどこで呼ばれているのかぱっと見てわからない」
という課題を感じます。
上記のような実装の場合、
「AがBを呼び出す」「BがCを呼び出す」
というような構造になっているため、上からちゃんと全部読まないと、どこで「C」が呼び出されているのかがわかりません。
今回は、サンプルコードでコードが短いため、比較的すぐに流れを追うことができると思いますが、
実際のプロジェクトのコードになると、
-
requestメソッド
のコールバックでエラーハンドリングや色々な処理が行われる - 今回は3つの処理しかしてないが、実際はもっと多くの処理を行う可能性がある
- 仕様変更でB処理とC処理の間に「B'処理」を入れたいなどが起こる可能性がある
といったコードが複雑化する要因が増えるため、サンプルコードよりも処理を追うことが困難になると予測されます。
処理フローを追いやすい順次処理の書き方
とりあえずコードはこんな感じ!
import UIKit
enum SplashTask {
case fetchVersionInfo
case fetchUserInfo
case fetchProductInfo
static func getTask() -> [Self] {
[.fetchVersionInfo, .fetchUserInfo, .fetchProductInfo]
}
}
class SplashViewController: UIViewController {
let api: APIManeger = APIManeger()
var splashTasks: [SplashTask] = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// getTask()によって、起動時の処理と順番を確定させる
splashTasks = SplashTask.getTask()
// 初回のみここで実行処理を呼び出す
executeSplashTask()
}
private func executeSplashTask() {
if splashTasks.count == 0 {
// タスクがなくなったので起動処理終了 -> トップ画面へ遷移
print("!!!! finish all")
return
}
let task = splashTasks.removeFirst()
switch task {
case .fetchVersionInfo:
fetchVersionInfo {
self.executeSplashTask()
}
case .fetchUserInfo:
fetchUserInfo {
self.executeSplashTask()
}
case .fetchProductInfo:
fetchProductInfo {
// この処理が最後なので念の為処理(整合性が取れてれば実際は不要)
splashTasks.removeAll()
self.executeSplashTask()
}
}
}
func fetchVersionInfo(completion: () -> Void) {
print("!!!! start version check")
api.requestVersionCheck() { result in
if result.isSuccess {
// バージョンアップが必要ならAppStoreに飛ばす etc...
// バージョンチェック正常終了
completion()
} else {
// エラーだったらアプリ終了
}
}
}
func fetchUserInfo(completion: () -> Void) {
print("!!!! start userInfo request")
api.requestUserInfo { result in
if result.isSuccess {
// ユーザー情報から必要な情報を取得したり、データを加工したりする処理 etc...
// 正常終了
completion()
} else {
// エラーだったらアプリ終了
}
}
}
func fetchProductInfo(completion: () -> Void) {
print("!!!! start productInfo request")
api.requestProductInfo { result in
if result.isSuccess {
// 商品情報情報から必要な情報を取得したり、データを加工したりする処理 etc...
// 正常終了
completion()
} else {
// エラーだったらアプリ終了
}
}
}
}
この順次処理の特徴は、
各リクエストメソッドが次にどのメソッドを呼び出すかを決定する責務を持っていないという点
です。
それにより、
リクエストメソッドの中身を覗かなくても、プログラマは順次処理の流れを把握することができるようになりました。
最初のコードでは
「AがBを呼び出す」「BがCを呼び出す」
という処理を各メソッドで行っていましたが、
今回は、リクエストが正常に完了したら、completion()
を呼び出して終わりです。
起動処理のコントロールは、executeSplashTask()
が行い、順序をコントロールしているのはSplashTask
という列挙型です。
executeSplashTask()
は、
Taskリストの先頭から切り出したTaskを実行し、Taskリストが空になったら、起動処理がすべて完了したとみなす仕組みを採用しています。
そして、自分自身を再帰呼び出しすることで処理を次に進めます。
まとめ
- 非同期処理を含む順次処理では、順序を決定するメソッドと実際の処理を行うメソッドに責務を分散させることで、プログラマは各処理の中身を知らなくても、順次処理の流れを理解することができる。
- 順所の変更や処理の追加といった仕様変更が発生しても、既存のロジックにほぼ影響を与えることなく修正が可能になる。