UiPathの RE Frameworkでも使われている、ステートマシンを理解する

## イントロ
UiPath Studio で処理を記述する方法として、一本の処理を書きやすい「シーケンス」と、処理の流れを視覚的に表現できる「フローチャート」、あとひとつは**いくつかの状態と、状態間を遷移するための条件を記述することでフローを記述する「ステートマシン」**があります。

UiPath Studioの新規プロジェクト作成画面にもある「Robotic Enterprise Framework (RE Framework)」の処理フローもこのステートマシンで書かれていますし、ちょっと整理しておこうかなと思いました。

ちなみに下記は、RE Framework のプロジェクトのサンプル。
![RE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/bf4d14ca-074c-bdfb-86de-936d1db109c5.png)


## ステートマシンとは

さてもうすこしステートマシンを説明すると「ある状態」があって

- その状態に入るときの処理を記述する： Entry
- その状態から出るときの処理を記述する： Exit
- その状態から別のある状態へ遷移するための条件/トリガーを指定する： Transition(s)

を駆使して、処理を進めていくしくみです。


以下主要の画面。
状態と、次の状態へ遷移する条件を記述
![02_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/27857753-dcf0-7667-1506-a82d1fcd1d74.png)

この状態に入るとき・出るときに実行される(Entry/Exit)
![02_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/483b1202-892e-4be3-2c04-95875e27b630.png)

遷移する条件を記述する(Transition(s))
![02_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/9a05fb17-0717-91d8-90d8-9534efa2e6cb.png)

### たとえば

たとえば、UiPath Academy にも出てくるおなじみの RandomNumberを当てるヤツ、コレはフローチャートで書かれてますが
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/b4730fbe-c554-c83a-fcb9-d4bc39c22cd3.png)

コレをステートマシンで記述すると、
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/ee4d0a9c-4191-a18b-a20a-553a1ca673d9.png)

こんな感じでしょうか。流れを説明すると、

- 初期化状態に入る(Entry) ときに ``RandomNumber`` を初期化
- 入力状態に入る(Entry) ときに、入力ダイアログを出してユーザ入力値を得る
- 下記の場合わけを遷移(Transition(s))として処理
    - 小さい場合 → 再入力
    - 大きい場合 → 再入力
    - 等しい場合 → 終了

などとなります。
ひとつの状態から**複数の遷移の条件(Transition(s))を定義できる**ようになっていることがわかりますね。

## やってみる

いくつかやってみます。

### 環境

UiPath Studio 2018.4.5で確認しています。

### まずはHello World レベルのもの

とりあえずとってもシンプルなところから。
``RandomNumber`` を初期化して、画面表示してみます。

- UiPath Studioのプロジェクトを作成し、シーケンスやフローチャートを置くかわりに「ステートマシン」アクティビティを追加。
- そのステートマシンの中にダブルクリックで入って、「ステート」と「最終ステート」アクティビティをひとつづつ配置
- Startアイコン → ステート → 最終ステートを線で結びます。
- 「ステートマシン」スコープで利用可能な ``RandomNumber`` 変数を定義します。


うまくいけばこんな感じになると思います。
![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/3a6bf7a8-e8f4-6cc9-da9b-f538020c7518.png)


つづいてステートをダブルクリック。Entry の枠に、アサインアクティビティを配置して``RandomNumber``を初期化(``RandomNumber = New Random().next(1,100)``)します。**このEntryはこの「ステート」状態に入ってきたときに実行される処理を記述**します。
![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/cb85ef47-1878-213f-5776-c37e2b9a98a4.png)

さいごに、最終ステートをダブルクリック。Entryの枠にメッセージボックスを配置しましょう。
![07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/147e1a68-c211-93bc-942d-c113a922278f.png)


実行してみましょう！、、、表示されましたね。
![08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/4be198b0-7b58-a85c-9a7b-ecbf0af9baf4.png)


ココまでのソースを取得したい場合は、
https://github.com/masatomix/UiPathStateMachineSample/releases/tag/helloworld
の「Source code (zip)」をダウンロードしてください。


### 分岐させる

ためしに「偶数はエラー」という処理を追加してみます。

まずは「最終ステート」アクティビティを追加して、名前を「偶数エラー」にします。そして、上のステートから矢印(図中T2)を繋ぎます。
![09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/4bb7894e-93cf-a3d0-94c6-27880f593e37.png)

エラーのマークが出ちゃいました。**遷移が複数になった場合は、Transition T1,T2にはかならず条件をつけないといけない**ようですね。というわけで、T1/T2をそれぞれダブルクリックして、Transitionを開き、

- T1: 偶数じゃなかったら(``RandomNumber mod 2 <> 0``) 遷移と設定
- T2: 偶数だったら(``RandomNumber mod 2 = 0``) 遷移と設定

とします。(下はT2の例)
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/b70b0fd0-5ac8-e195-1d17-760349201b51.png)

エラーのマークが消えたと思います。
つづいて、先ほど置いた「偶数エラー」をダブルクリックして中に入って、
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/fa926b9f-e9bb-bcbd-c55c-329da669c9a4.png)

メッセージボックスを置けばOK。完成です。
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/b612dc0d-4d6b-100c-de59-0d862e42766f.png)

実行してみると、偶数の時だけエラーメッセージが表示されます。
![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/d3bed1b0-0934-82eb-a7db-68160a35c996.png)

ってことで、初期の「ステート」の状態から、Trasition(s)に定義した条件に沿って別の状態「偶数エラー」「最終ステート」に遷移させられることがわかりました。
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/d6b6c37b-8255-69c5-49a9-de55ea74c451.png)

ココまでのソースを取得したい場合は、
https://github.com/masatomix/UiPathStateMachineSample/releases/tag/odd_and_even
の「Source code (zip)」をダウンロードしてください。

### 注意: 条件分岐の評価の順番

さてさて、Transition(s)のT1/T2ですが、

- T1: 偶数じゃなかったら(``RandomNumber mod 2 <> 0``) 遷移と設定
- T2: 偶数だったら(``RandomNumber mod 2 = 0``) 遷移と設定

と**モレなくダブりなくMECEに設定した**わけですが、ダブりありで設定したい場合もありますよね。「4の倍数の場合T3、偶数の場合T2、それ以外の場合T1」とかです。
![MECE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/81baa024-4572-17a3-f1ad-2b6afcf67707.png)


このような場合、**Transition(s)に設定した条件が評価される順番が気になります**が、結論をいうと右部の**概要ペインに並んでいる順番になるっぽい**んですよね。上記のキャプチャの例だと、T1,T2,T3の順番で評価されるので、**T3の「4の倍数の条件」は評価されない**って事になります。。この順番の制御が緻密に出来れば「**まあプログラム書くときもif文の並び変えればアルゴリズムかわるよね**」ってのと同じ、と割り切りますが、この概要ペインの**順番の制御はGUI上でTransitionの矢印をドラッグしたりするとかわる(いじったTransitionが最後にいく)** とっても繊細な挙動。。というか逆にTransitionの矢印をいじる以外の制御方法がよく分かりません。。

↓こうやってドラッグすると、T3が最後に移動したりする(あ、最初からT3が最後で、、例がイマイチでしたね。)。
![DND.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/8dddd312-ca5e-57c9-05e8-1bc213e9e78f.png)


さらに、ダブりではなく「モレ」があった場合、**モレの条件になると遷移先が決まらず、ダンマリ**になってしまいます。偶数奇数の判定で「偶数かそれ以外」って書ければと思いましたが、**それ以外つまりSwitch文でいうdefault節のような仕様もなさそう**で、、なんだかこのへんの仕様はとても微妙だよねぇ、とおもいます。。

まあ、**遷移を制御する条件はモレなくダブりなく設定する**ようにしましょう、という結論になりそうです。


### 遷移する条件には、トリガー待ちを設定出来る

ステートマシンは、Transition(s)で指定した条件の評価を行うタイミングを制御するトリガーを張ることができます。**ある指定したイベントの発生をトリガーにして、次の状態に遷移するための条件チェックを実行する** ことができるわけですね。

たとえばいわゆる**ファイル待ちトリガー**のように「所定の箇所にファイルが置かれたら、次の状態へ遷移する」などです。


やってみます。

- トリガーファイル ``c:/temp/data.trg`` が更新されるのを待つ
- 更新されたら、次の処理を実行 (処理は、``c:/temp/data.txt``の中身を表示するだけ)
- トリガー更新まちに戻る。
- トリガーファイルの中身が「end」で更新されたら、次の処理を最後に終了する

なんてのをつくってみましょう。


#### STEP1: ファイル待ちの設定

先のプロジェクトのステートたちは一旦削除。

「初期」「メイン処理」「終了」というステートを追加。スタートからそれらをTransitionで繋ぎます。また、メイン処理は繰り返すので処理へ戻るTransitionも追加します。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/0fcb4715-e313-8aef-026b-44248f62f902.png)


トリガーファイルが来たらメイン処理を実行するので、トリガーまちを仕掛けるのは図中の T1 が良さそうです。なので T1 をダブルクリックして

- 上部のTriggerに「イベントを監視」アクティビティ
- そのなかに「ファイル変更トリガー」を配置
- イベントハンドラの中は「テキストファイルを読み込む」アクティビティを配置

します。
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/bc69804b-cf50-971f-d553-89a5c81781e0.png)


それぞれのアクティビティに値を設定していきますが、、、その前にまず変数を定義しておきます。


| 変数名            |      型 |スコープ|    規定値    |
|:-----------------|:------------------|:---|------------------|
| TriggerData      |            String |ステートマシン|            |
| Path             |            String |ステートマシン|     "c:/temp/"        |
| TriggerFile      |            String |ステートマシン|       "data.trg"       |

![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/0d94871e-5e41-7e97-1320-ccdae4d36ef2.png)


さて、それぞれのアクティビティに値を設定していきます。

「イベントを監視」アクティビティ:

- 「無限に繰り返す」プロパティをFalse

とします。
![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/6eaf8453-979b-71a8-0996-429e4b15c444.png)

「ファイル変更トリガー」アクティビティ:

- 通知フィルタ を ``Size``
- パス を ``Path``
- ファイル名 を ``TriggerFile`` 

とします。
![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/ca06ee86-a033-cccf-983b-cb10d1906a73.png)


イベントハンドラ内の「テキストファイルを読み込む」アクティビティ:

- ファイル名: System.IO.Path.Combine(Path,TriggerFile) ←パスを連結するだけ
- コンテンツ: TriggerData

とします。
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/079e5349-beb5-5f35-fcbf-e7fa379b6eff.png)


エラーが消えましたね。以上で

**トリガーファイル ``c:/temp/data.trg`` が更新されたら、メイン処理 状態へ遷移する**ところが完成です。ちなみに、トリガーファイルの中身が「end」だったら〜、を実装するためにトリガファイルの内容は ``TriggerData``変数に格納するようにしました。



#### STEP2: メイン処理

ステートマシン全体図に戻って「メイン処理」をダブルクリック。この「メイン処理」に入ってきたときにうごくのは「Entry」節なので、そこで「c:/temp/data.txt」を読んでメッセージボックスを出す処理を記載します。だいたいこんな感じ[^1]。
![21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/1b89f365-3e98-18e0-ad98-bfa78f66b5b7.png)

[^1]: resultって変数定義したりとか、細かい説明は省略です:-)


#### STEP3:トリガーファイルの中身に「end」と書いてあったら、次の処理を最後に終了する

最後に終了条件の仕様の実装です。Transition T2に設定すればよさそうです。T2をダブルクリックします。T2に進む条件に``"end".Equals(TriggerData)``と設定します。
![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/c0d513bf-ca5f-ab10-90b3-1d6a1d1b34ab.png)

Transition T3は、それ以外って条件を追加する必要があります。さきほど書いたとおり**Switch文のdefault節は存在しない**ので、それ以外は``not "end".Equals(TriggerData)`` としました。
![23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/f9d8f7c1-f213-073a-1c0a-b7be0c0837df.png)


下記の通り、エラーも消えたとおもいます。ではワークフローを実行してみましょう。
![24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/85df335c-1cc9-bc46-27bf-a7468e577ccb.png)

ワークフローは起動後トリガまちになるので「``c:/temp/``」ディレクトリにトリガーファイル「``data.trg``」とデータファイル「``data.txt``」を置いて、いろいろと動かしてみてください。トリガファイルを更新して保存する度に、データファイルが読み込まれて画面に表示されると思います。またトリガーファイルに「end」という文字を書いて保存すると、データファイルを処理したと、ワークフローが終了することを確認してください。
![25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/db78d3f0-2d7a-4c3d-9f87-a70fd1167c38.png)


以上トリガーファイルを用いたステートマシンのサンプルでしたー。。

ココまでのソースを取得したい場合は、
https://github.com/masatomix/UiPathStateMachineSample/releases/tag/trigger
の「Source code (zip)」をダウンロードしてください。



あ、そうだ、Transition(s)のラベルはわかりやすい表記にしておくとよいと思います。
![LABEL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/58936df7-0fcc-4c56-dd42-606d011f387a.png)



## ちなみにRE Framework はザックリこんな感じ

さてさて、ステートマシンがある程度理解できると、また簡易版のRE Frameworkである「Attended Framework」(参考:[Attended Framework テンプレートを使ってみた](https://qiita.com/masatomix/items/1d96169a7c423b1939b0)) の知識もあわせてみてみると、下記のRE Framework のステートマシンも、ある程度理解が出来るかもしれません。
![RE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/bf4d14ca-074c-bdfb-86de-936d1db109c5.png)
だいたい、こんな感じです

- Initステートでは、**[Attended Frameworkとほぼおなじ仕組みで設定ファイルの読み込み](https://qiita.com/masatomix/items/1d96169a7c423b1939b0#%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%AE%E6%A8%99%E6%BA%96%E5%8C%96)**を行っている。**例外が発生したら``SystemError``変数にその例外をセット**している
    - ``SystemError``変数 が Nothingでないばあい、Transition:「System Error」で、End Processステートへ遷移して終了
    - ``SystemError``変数 が Nothingのばあい、Transition:「Success」で、Get Transaction Data ステートへ遷移
- Get Transaction Data ステート は**Entryで Orchestratorのキューへデータを取得**しに行く。データが取れたら ``TransactionItem``へ格納。取れなかったりエラーだったらその変数はNothingのまま。
    - ``TransactionItem``変数 が Nothingのばあい、Transition:「No Data」で、End Processステートへ遷移して終了
    - ``TransactionItem``変数 が Nothingでないばあい、Transition:「New Transaction」で、Process Transaction ステートへ遷移
- Process Transaction ステートはEntryで、**プロジェクトごとの処理となる「Process.xaml」を実行**。
- **(「Process.xaml」は業務例外が発生した場合は「BusinessRuleException」をスローするルールとしておきます(コレはAttended Frameworkでもおなじルールでしたね))**。
- Process Transaction ステートは「Process.xaml」がスローしてきた**BusinessRuleExceptionは``BusinessRuleException``変数にセット**している。「Process.xaml」がスローしてきた**その他のExceptionは``SystemError``変数にセット**している。
    - ``SystemError``変数 が Nothingでない場合、Transition:「Error」で、Initステートへ遷移 (初期化からやり直しとなるが、**キューにデータがリトライとして再投入されているので、結果再ラン**)
    - ``BusinessRuleException``変数 が Nothingでない場合、Transition:「Rule Exception」で、Get Transaction Data ステートへ遷移 (**該当データの処理はあきらめて、次のデータを取得**)
    - ``SystemError``変数 も、``BusinessRuleException``変数 も Nothingの場合は正常終了として、Transition:「Success」 でGet Transaction Data ステートへ遷移 (**次のデータを取得**)
    - (条件の評価の順番も、上の通り。ただMECEになってるから、順番変わっても問題なさそう)


ながかったですが、一つ一つ整理すると、こんなかんじとなります。。よくよく読んでみると、Transitionの条件指定はキレイに「モレなくダブりなく」な定義となっていました。。




## まとめ

さてステートマシンやその他のナレッジをまとめておきます。

- ある「状態(State)」と状態間を「遷移する条件(Transition)」をつかって、比較的複雑なワークフローを記述するための仕組み。
- 条件を評価する前にトリガーを張ることで、ファイルが到達したら処理を開始する、なんて事も可能で、とても便利。
- 条件の評価は、評価順がとっても微妙な仕様なので、モレなくダブりなくを心がけた方が安全。マウスでフローをいじったら評価順がかわってバグっちゃう、とかヤバすぎる
- RE Frameworkはステートマシンで処理フローが記述されているが、ザックリとしたフローは割とシンプル
- よくよむと Attended Frameworkと似ている箇所が多数あり(設定ファイルよみこみ、例外処理の考え方)、Attended Frameworkから入ると理解しやすいかも。


おつかれさまでしたーー。。

## 関連リンク

- [UiPath Developer Community 第12回](https://qiita.com/masatomix/items/d8839b21cb09513e19ef) ここでRE Frameworkと共通点もおおいAttended Frameworkについて、お話しさせていただきました。
- [Attended Framework テンプレートを使ってみた(Slide版)](https://qiita.com/masatomix/items/8ef146486a87686a9345) そのときの資料
- [Attended Framework テンプレートを使ってみた](https://qiita.com/masatomix/items/1d96169a7c423b1939b0) その資料の元ネタ
- https://github.com/masatomix/UiPathStateMachineSample/releases/tag/Lesson2 たとえば、に書いたUiPath Academyの題材でもある推測ゲームを実装したサンプル
- [UiPath Developer Community 第9回「Error Handling」](https://qiita.com/masatomix/items/38adb155327d287e4248) RE Frameworkのエラーハンドリングはここで拝見したスライドがとても参考になりました。