プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

ソフトウェアの状態管理と状態遷移表

Windowのハードディス残量のデイリーの変化を記録して、残量が少なくなるとアラートがでるアプリを作ってました。
f:id:s51517765:20191123145009j:plain
主な特長:
1)ディスク残量をテキストファイルに出力します。
2)監視するドライブを指定できます。
3)残り容量が少なくなった時アラートを出します。音は出ません。

ドライブを指定することが出来るのですが、外付けのディスクを指定して、それが接続されていないと残量0となり意図しない挙動をしてしまいました(容量も0)。
すると、ディスクが準備可能かどうかを確認し、警告を発し済みかどうかを管理し…、といった複雑な挙動が必要になりました。

こうなってくると、複数のif文で管理することが難しくなります。

そこで思い出したのが、状態遷移表(状態管理)です。

表の書き方の流儀は大きく2つあります。
・縦軸に現在の状態、横軸にトリガーを置き、表の中に遷移先を示すもの。
・もう一つは縦軸に現在の状態、横軸に次の状態、表の中にトリガーを示すものです。

どちらがいいかは、ソフトウェアによって、コーディングの仕方によっても異なるかと思いますが、私は前者を使うことにしました。

挙動説明

これが、このソフトウェアの状態遷移表です。
f:id:s51517765:20191123151432p:plain
状態遷移表
まず、ソフトウェアが起動されるとStart状態になります。
自動遷移で、DiscCheckに移ります。
ディスクが準備完了(アクセス可能状態)であれば、ディスクチェックをして、Idleに遷移します。
ディスクが準備完了していない場合は、その状態に留まり、ディスクの準備完了を待ちます。
Idle状態では120s経過で自動終了します。
Idleはなにもしない状態です。

これが正常の動作フローです。

一方、DiscCheck状態で、20s経過しても準備完了にならない場合は、Alertに遷移し、メッセージを通知し、Idleに遷移します。Idleに遷移したあとは、正常時と同じです。

手動でディスクチェックをしたい場合があるので、手動用のボタンがあります。このときの挙動もトリガーGetRemainで記述しておきます。
f:id:s51517765:20191123150436j:plain
この時もディスクが準備完了である必要があります。これを遷移条件(進入条件)といいます。
状態遷移するときに何かの動作を行わせたいときは、出口アクション(そんな言葉はないかもしれないし、状態遷移表で記述するものではないかも)として記述しています。

監視対象ディスクを変更したときも、準備完了を待つためにタイマーをリセットします。

コード例

timerをつかって、2秒ごとに現在の状態に応じた動作を行います。
上記の状態がSwitch.Caseで分けられています。
何もしないとき(ここではIdle状態)も記述しておきます。

ifで遷移条件を確認しActionし状態遷移します。
これは、ソフトウェアが起動している間、常に2sごとに実行されます。
時間で動作を分けたいとき(ここでは120sで終了するなど)はswitchの外でカウントアップします。

        private void timer1_Tick(object sender, EventArgs e)
        {
            timeCount++;

            switch (state)
            {
                case "DiscCheck":
                    {
                        if (getRemain())
                        {
                            state = "Idle";
                        }
                        else if (timeCount > 10) //20s
                        {
                            state = "Alert";
                        }
                    }
                    break;

                case "Idle":
                    {
                        //120s後
                        if (checkBoxAutoClose.Checked && timeCount > 60)
                        {
                            state = "Close";
                        }
                    }
                    break;
                case "Alert":
                    {
                        state = "Idle"; //先にAlertをしようとすると、次のタイマーが回ってきて複数のAlertが上がってくる。次の状態を変更してから動作する。
                        Alert();             
                    }
                    break;

                case "Close":
                    {
                        this.Close();
                    }
                    break;

                default:
                    {
                        MessageBox.Show("Error");
                    }
                    break;
            }
        }

まとめ

状態遷移表を使ったらうまくまとめることが出来ました、という事例です。
タイマーで現在の状態と次の状態を確認し続ける、というのがポイントです。
これに似たものとして状態遷移図というものもあります。
場合によっては両方描いたほうがいいかもしれません。
状態遷移図のほうが描きやすくはありますが、コードに落とし込むにはもう一段階が必要になり、それが状態遷移表だと思います。
状態遷移図はちょっと複雑なフローチャートのようなものです。
状態遷移図も状態遷移表もUMLの一つで、厳密には書き方のルールが存在します。
この記事では必ずしも正確な書き方ではありませんのでご了承ください。

時間がかかる処理をするとき、ここではAlertです(ここではMessageBoxをだすので、ユーザーが処理するまでの時間がかかる)が、このときは先に状態を変更しておかないと、2s後にもう一度Alertが呼ばれます。
このような場合は、タイマーを一時的に止めるといった荒業もあります。

このようなソフトウェアのデバックをするときは、状態遷移表のすべてのマスを確認します。
起こりえないマスはそのために明確にしておきます。