LoginSignup
40
33

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-10-02

この記事は

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は単純さを保てる

参考

40
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
33