post Image
ライブラリを使わずにMV*の話(iOS)〜Modelに状態を持たせて状態遷移を行う〜

この記事は

MV*というかModelの話がメインです

結論

  • Modelに 状態 を持たせ、 状態遷移 を行うようにする。(= Modelをステートマシンとして使う)

話すこと

  • ステートマシンとは
    • 状態とは
    • 状態遷移とは

前提

  • Model
    • ユーザーに提示する状態・値を持っている
    • 内部状態の変更を通知(Observerパターン)で知らせる

参考?

この記事のみでも成り立つように作成していますが、上記の前提を詳しく話しているのは別の記事になります。

ステートマシンの話に入る前に

なぜステートマシンが必要になるのか。
アプリ開発のよくある要件を例に話を始めます。

よくある要件

  • 画面上にボタンがある
  • ボタンを押したらサーバーと通信して情報を取得する
  • 通信中 -> インジケータを表示したい
  • 通信中は、再度ボタンをタップされても何もしないようにしたい
  • 通信失敗 -> アラートを表示したい
  • 通信成功 -> 結果を画面へ反映したい

それぞれをフラグ管理した場合

要件を実現するために、Modelにフラグを用意してみます。

class ProcessModel {

    // 通信中フラグ
    private(set) var isDoing: Bool

    // 成功時は結果のStringが入る
    private(set) var result: String?

    // 失敗時はErrorが入る
    private(set) var error: Error?

    func execute() {
        // 通信中の場合は何もしない
        guard !self.isDoing else {
            return
        }
        self.isDoing = true

        /**
         なんらかの非同期な通信処理
         コールバックで通信結果を受け取るとする
         */
        API.someProcess { processResult in
            // 通信成功の場合
            if processResult.isSuccess {
                self.result = self.createResult(from: processResult)

            // 通信失敗の場合
            } else {
                self.error = self.createError(from: processResult)
            }
        }
    }

}

Modelの取りうる状態

フラグが3つありますが、この場合Modelの取りうる状態は全部で何パターンあるでしょうか?

No. isDoing result error
1 true nil nil
2 true nil Error
3 true String nil
4 true String Error
5 false nil nil
6 false nil Error
7 false String nil
8 false String Error

全8パターン。
実はこの時点で既に問題が発生しています。
個別のフラグ管理 = 「このModelはどういう状態をとるのか」の全パターンが分かりづらい という問題です。

そして良く見ると、どういう状態なのかよく分からないパターンが…。
No.2: 通信中かつエラーがある場合?
No.3: 通信中かつ結果がある場合?
No.4,8: 結果もエラーもある場合?

既によくわからないパターンが含まれていますが、ここからさらに仕様を追加されたらどうなるでしょうか?

さらにフラグが増えた場合

仕様追加「処理Aが実行中は処理Bを行わないようにしたい」
-> isProcessADoing, isProcessBDoing の発生

フラグが増えたので、パターン数も増えました。
ついでに、あり得なさそうな(あり得てはいけないはずの)状態も増えました。
(isProcessADoing = true かつ isProcessBDoing = true とか)

つまり、 フラグが増える = 無駄なパターンも増える

きつい💀

問題点と要望

  • 問題点

    • 現状では「このModelはどういう状態をとるのか」の全パターンが分かりづらい
    • フラグが増える = 無駄なパターンも増える
  • 要望

    • 取りうる状態の一覧が欲しい
    • 無駄なパターンは省きたい

Enumが便利ですよ

要望を叶えるためにはどう変更したらいいか考えてみます。

「取りうる状態の一覧が欲しい」
-> Enumが良さそう。

ということで、状態を表すのにEnumが便利なので使います。
しかし今度は、どういうEnumを定義すればいいのかという疑問が出てきます。

そこで、状態(Enum)を持ってるModelのことを、ステートマシンとして使うと良いよという話をします。

ステートマシンとは

  • 状態を持ち、 状態遷移を行うクラス
  • 簡単に言うと、状態遷移図を実装したクラス

状態とは

通信中, 通信失敗, 通信成功 などを指しています。

状態遷移とは

文字通り状態が移り変わることです。

要件を満たす状態遷移図はこんな感じ
スクリーンショット 2017-10-06 14.26.02.png

Modelをステートマシンとして使う = 上記の状態遷移図を満たすようにModelを実装する
ということになります。

具体的なコードを書いてみる

状態の一覧をEnumで定義する

状態遷移図を元に、取りうる状態をEnumで定義します。

State
enum State {
    // 何もしていない
    case sleeping

    // 通信中
    case doing

    // 通信成功
    case success(result: String)

    // 通信失敗
    case failure(error: Error)
}

状態の定義はこれでOK…?🤔

状態遷移図を見直してみる

通信中のあと
-> 通信成功 or 通信失敗
-> 通信成功 と 通信失敗は同列で扱うべきっぽい

下記のように変更

State
enum State {
    case sleeping
    case doing
    case done(Result)

    enum Result {
        case success(result: String)
        case failure(error: Error)
    }
}

状態の定義はこれでOK🎉

Modelに状態、状態遷移を持たせる

定義した状態を使うようにModelを書き換えます。

ProcessModel
class ProcessModel {
    // 状態をModelに持たせる
    // 状態: 何もしていない
    private(set) var currentState: State = .sleeping

    func execute() {
        // 状態: 通信中
        self.currentState = .doing

        /**
         なんらかの非同期な通信処理
         コールバックで通信結果を受け取るとする
         */
        API.someProcess { processResult in
            // 通信結果: 成功
            if processResult.isSuccess {
                let result = self.createResult(from: processResult)

                // 状態: 通信成功
                self.currentState = .done(.success(result: result))

            // 通信結果: 失敗
            } else {
                let error = self.createError(from: processResult)

                // 状態: 通信失敗
                self.currentState = .done(.failure(error: error))
            }
        }
    }

}

これで状態遷移図を満たせているでしょうか。

連打対策

ぱっと見は大丈夫そう。
ですが、実は状態遷移図にない 余計な遷移 をしてしまっている箇所があります。

現状を表す状態遷移図
スクリーンショット 2017-10-06 14.25.40.png

通信中 -> 通信中 の矢印です。
この矢印があると、 要件の「通信中は、再度ボタンをタップされても何もしないようにしたい」を満たさないので、Modelを修正します。

class ProcessModel {
    // ... 中略 ...

    func execute() {
        switch self.currentState {
        case .doing:
            // 何もしない
            return
        case .sleeping, .done:
            // 状態: 通信中
            self.currentState = .doing
        }

        // ... 以下同じ...
    }
}

要望は満たせたか?

  • 取りうる状態の一覧が欲しい
    • ⭕️ -> Enumが一覧になる
  • 無駄なパターンは省きたい
    • ⭕️ -> 状態遷移図を書いた時点で省かれている

yeei😎

処理が複数ある場合は状態が複数必要なのでは?

仕様追加「処理Aが実行中は処理Bを行わないようにしたい」に対応するためには?

処理ごとにModelをわけ、2つのModelを取り持つModelを作る

処理A, 処理BごとにModel作り、Model同士を取り持つクラスをつくります。
「取り持つクラス」も立ち位置はModelなのですが、ただのModelと区別するためにModelMediator(Modelの仲介者)と名付けています。
(名前はModelでもいいです(宗教感ある))

ModelMediator
class ModelMediator {

    private let processAModel: ProcessAModel
    private let processBModel: ProcessBModel

    // 要件: 処理Aが実行中は処理Bを行わないようにしたい
    func executeProcessB() {
        switch self.processAModel.currentState {
        case .doing:
            // 処理Aが実行中なので何もしない
            return

        case .sleeping, .done:
            self.processBModel.execute()
        }
    }
}

Modelへ指示する側では、直接Modelを持たずにModelMediatorを持ちます

Controller
class Controller {
    // ModelMediatorを持つ
    private let modelMediator: ModelMediator

    init(modelMediator: ModelMediator) {
        self.modelMediator = modelMediator
    }

    func executeProcessB() {
        // 各Modelが今どういう状態か、はControllerで考慮しない -> ModelMediatorにやらせる
        self.modelMediator.executeProcessB()
    }
}

Modelを分ける意味はあるのか

Q. Model同士を取り持つクラスがいるなら、初めから1つのModelにしてしまった方が楽なのでは?
A. 処理Aと処理Bは別のものなので、別Modelにできるはず。別Modelのままにすることで、それぞれのModelの単純さを保ちたい。

処理Aは実行中に処理Bのことは考えません。逆も同じです。
処理Bは開始時こそ処理Aのことを考えますが、実行を開始した以降は処理Aのことを考えません。
処理Aと処理Bが関係するのは、「処理Bの開始時に処理Aが実行中だった場合」だけです。

ある一点でしか関係しないのでしたら、「その一点を表すクラス(Model)」を外部に用意する方が、各Modelは単純で済みます。

まとめ

  • Modelをステートマシンとして使うと便利
    • 状態の一覧があるので、 Modelの取りうる状態がわかりやすい
    • 無駄なパターンを省ける
  • Modelが複数必要になった場合は、取り持つクラスをModelの外部につくる。そうすることで、それぞれのModelは単純さを保てる

参考


『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。