今日は、Objective-C のプロトコルを駆使して有限オートマトン(finite automaton)、別名:有限状態機械(finite state machine)を作りつつ、オブジェクト指向プログラミングに慣れていこうと思う。
とは言ってもそれほど難しいものではなく、今回作るのは、ユーザの入力によって以下のように状態が遷移する、簡単な『お買い物プログラム』だ。
※ 突っ込みどころが満載なのはデフォなので無視してほしい。
最初に断っておくと、状態遷移を管理するプログラムの実装自体は、特別なテクニックが無くとも(一応)可能である。
上記の状態遷移を直線的に列挙すると、おおむね以下の通り。
1. 『いらっしゃいませ』 A. 買いにきた(→2) B. 売りにきた(→9)
2. 『お買い上げですね』 A. 武器(→3) B. 防具(→6)
3. 『どれにいたしますか?』 A. 剣(→4) B. 銃(→5)
4. 『剣でよろしいですね?』 A. はい(→10) B. いいえ(→3)
5. 『銃でよろしいですね?』 A. はい(→10) B. いいえ(→3)
6. 『どれにいたしますか?』 A. 鎧(→7) B. 兜(→8)
7. 『鎧でよろしいですね?』 A. はい(→10) B. いいえ(→6)
8. 『兜でよろしいですね?』 A. はい(→10) B. いいえ(→6)
9. 『本当に売りますか?』 A. はい(→10) B. いいえ(→10)
10. 『ほかに御用はありますか?』 A. はい(→1) B. いいえ(→11)
11. 『ありがとうございました』 (→End)
この処理を素直に実装しようとすると、11種類の状態とその遷移先をすべて if 文で振り分ける方法がまず思いつく。
敢えてこの方法のソースコードは示さないが(ホント面倒なので)、気になる方には是非実装して頂きたい。間違いなく途中で厭になる。
上記方針で実装されたプログラムは、ソースコードの可読性だけでなく、拡張性までをも失う事になる。新たに状態を追加したくなったときには、おびただしい if 文に手を加える必要があるからである。もちろん、状態が正しく遷移する事を確認するのも大変だ。
しかし、1つ1つの状態が共通の振る舞いを持っている事に気付けば、オブジェクト指向の考え方を用いてよりスマートに実装できる。
簡単のため、こんなオートマトンを考えてみよう。
まず、共通の振る舞いをまとめた抽象クラス State を作る。各状態の共通の振る舞いは《次に遷移する状態を決定する》という更新処理であり、ここでは update メソッドと名づけておく。
// =============================================== // 抽象クラス(…に相当するもの。継承して使う) // =============================================== @interface State : NSObject - (State *) update; @end @implementation State - (State *) update { return (State *)Nil; } @end
Objective-C には、抽象クラスを作るための特別な構文がないので、傍目には普通のクラスと見分けがつかない。そのため、不慮のバグを見つけやすくするために State クラスは常に Nil を返すようにしている。
「どういうときに・どこへ遷移するか」は、各状態によって異なるため、State を継承した状態クラス State1、State2、State3 を定義する。update メソッドをオーバーライドし、個別に更新処理を記述すればよい。
戻り値が State * 型なのは、update メソッドが次に遷移する状態のインスタンス(のポインタ)を返すようにするためである。
// =============================================== // 各状態クラス ~インタフェース部~ // =============================================== @interface State1 : State - (State *) update; @end @interface State2 : State - (State *) update; @end @interface State3 : State - (State *) update; @end // =============================================== // 各状態クラス ~実装部~ // =============================================== @implementation State1 - (State *) update { printf("ここは状態1です∩( ・ω・)∩\n"); getchar(); return [[State2 alloc] init]; } @end @implementation State2 - (State *) update { printf("ここは状態2だよヾ(・ω・)ノ゛\n"); getchar(); return [[State3 alloc] init]; } @end @implementation State3 - (State *) update { printf("ここは状態3である(´-ω-`)\n"); getchar(); return [[State2 alloc] init]; } @end
これで準備は完了だ。各状態クラスをこのように作っておくだけで、main 関数はこんなにシンプルに書ける。
main() { State *state = [[State1 alloc] init]; State *next; while(next = [state update]) { if(state != next) { [state release]; state = next; printf("遷移しました!\n"); } } }
ちなみに実行すると、こうなる(何か入力があるたびに、問答無用で次の状態へと遷移する)。
状態遷移図の通り、初期状態 1 から、以降、状態 2 と 3 を交互に遷移している事が判る。
【プログラムのからくり】
State から派生した State1、State2、State3 のインスタンスは、いずれもアップキャストする事で State と同一視して扱う事が可能になる。
この性質を利用して、main の中でポインタの継ぎ換えを行い、状態遷移を実現するのだ。ここだけ詳しく見ていこう。
はじめに、State *state = [[State1 alloc] init]; で、変数 state には状態 1 のポインタが格納される。
この状態で [state update] を実行すると、状態 1 のインスタンスが次状態(状態 2 )のインスタンスを生成し、戻り値として返す。
これを、next に一時的に格納する。
実は、Java ではこの一時変数は必要なく、いきなり state に次状態を格納しても大した問題は起きないのだが、Objective-C で同じようなマネをしたら大問題になる。そう、メモリリークだ。
そこで、state と next を比較して、異なる先を参照している場合は、ちゃんと release をしてからポインタを継ぎ換える必要がある。
これによって state の中身はどんどん更新され、update を実行すると、(現在の状態に応じて)振る舞いが変わる。この多態性と呼ばれる性質のお陰で、現在の状態が何であるかをプログラマが意識する事はほとんどなく、スマートにプログラムを記述できるのだ。
これが、オブジェクト指向による状態遷移実装のからくりである。
ちなみに、抽象クラスではなく、プロトコルを用いて上記のプログラムを書き換える事ができる。
#import <Foundation/Foundation.h> // =============================================== // プロトコル // =============================================== @protocol State // 更新メソッド // 次の状態(へのポインタ)を返す - (id) update; @end // =============================================== // 各状態クラス ~インタフェース部~ // =============================================== @interface State1 : NSObject<State> - (id) update; @end @interface State2 : NSObject<State> - (id) update; @end @interface State3 : NSObject<State> - (id) update; @end // =============================================== // 各状態クラス ~実装部~ // =============================================== @implementation State1 - (id) update { printf("ここは状態1です∩( ・ω・)∩\n"); getchar(); return [[State2 alloc] init]; } @end @implementation State2 - (id) update { printf("ここは状態2だよヾ(・ω・)ノ゛\n"); getchar(); return [[State3 alloc] init]; } @end @implementation State3 - (id) update { printf("ここは状態3である(´-ω-`)\n"); getchar(); return [[State2 alloc] init]; } @end main() { id state = [[State1 alloc] init]; id next; while(next = [state update]) { if(state != next) { [state release]; state = next; printf("遷移しました!\n"); } } }
なお、抽象クラスのときと違って、update の戻り値を State * (プロトコルのつもり)にしてしまうと、なぜか Sytax Error になってしまう。恐らく、プロトコルは型名として扱われない仕様なのだろう。戻り値の型を id 型に変更したらエラーが消えた。
これはインタフェースにアップキャストできる Java や C++ では考えられない現象だったので、ちょっとだけ嵌ったポイントでもある。
ここまで書けば、あとは『お買い物プログラム』の作成は簡単である。
一応すごく適当に作ったソースを載せておくが、もうちょっと各状態クラス名をマシなものにすれば、一瞥しただけで遷移先が把握できるようになると思う。
#import <Foundation/Foundation.h> // =============================================== // プロトコル // =============================================== @protocol State - (id) update; @end // =============================================== // インタフェース部 // =============================================== // 『いらっしゃいませ』状態 @interface IrasshaiState : NSObject<State> {} - (id) update; @end // 『お買い上げですね』状態 @interface OkaiageState : NSObject<State> {} - (id) update; @end // 『武器はどれにいたしますか?』状態 @interface BukiSelectState : NSObject<State> {} - (id) update; @end // 『剣でよろしいですね?』状態 @interface TsurugiState : NSObject<State> {} - (id) update; @end // 『銃でよろしいですね?』状態 @interface JuuState : NSObject<State> {} - (id) update; @end // 『防具はどれにいたしますか?』状態 @interface BouguSelectState : NSObject<State> {} - (id) update; @end // 『鎧でよろしいですね?』状態 @interface YoroiState : NSObject<State> {} - (id) update; @end // 『兜でよろしいですね?』状態 @interface KabutoState : NSObject<State> {} - (id) update; @end // 『本当に売りますか?』状態 @interface UrimasukaState : NSObject<State> {} - (id) update; @end // 『ほかに御用はありますか?』状態 @interface HokaninanikaState : NSObject<State> {} - (id) update; @end // 『ありがとうございました』状態 @interface ArigatouState : NSObject<State> {} - (id) update; @end // =============================================== // 実装部 // =============================================== // 『いらっしゃいませ』状態 @implementation IrasshaiState - (id) update { int inputValue; // メッセージ表示 printf("いらっしゃいませ\n"); printf("1. 買いにきた 2. 売りにきた\n"); // 入力受付 scanf("%d", &inputValue); if(inputValue == 1) { // 買いにきた場合 return [[OkaiageState alloc] init]; } else if(inputValue == 2) { // 売りにきた場合 return [[UrimasukaState alloc] init]; } // それ以外は遷移しない(自分自身を返す) return self; } @end // 『お買い上げですね』状態 @implementation OkaiageState - (id) update { int inputValue; printf("お買い上げですね\n"); printf("1. 武器 2. 防具\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[BukiSelectState alloc] init]; } else if(inputValue == 2) { return [[BouguSelectState alloc] init]; } return self; } @end // 『武器はどれにいたしますか?』状態 @implementation BukiSelectState - (id) update { int inputValue; printf("武器はどれにいたしますか?\n"); printf("1. 剣 2. 銃\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[TsurugiState alloc] init]; } else if(inputValue == 2) { return [[JuuState alloc] init]; } return self; } @end // 『剣でよろしいですね?』状態 @implementation TsurugiState - (id) update { int inputValue; printf("剣でよろしいですね?\n"); printf("1. はい 2. いいえ\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[HokaninanikaState alloc] init]; } else if(inputValue == 2) { return [[BukiSelectState alloc] init]; } return self; } @end // 『銃でよろしいですね?』状態 @implementation JuuState - (id) update { int inputValue; printf("銃でよろしいですね?\n"); printf("1. はい 2. いいえ\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[HokaninanikaState alloc] init]; } else if(inputValue == 2) { return [[BukiSelectState alloc] init]; } return self; } @end // 『防具はどれにいたしますか?』状態 @implementation BouguSelectState - (id) update { int inputValue; printf("防具はどれにいたしますか?\n"); printf("1. 鎧 2. 兜\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[YoroiState alloc] init]; } else if(inputValue == 2) { return [[KabutoState alloc] init]; } return self; } @end // 『鎧でよろしいですね?』状態 @implementation YoroiState - (id) update { int inputValue; printf("鎧でよろしいですね?\n"); printf("1. はい 2. いいえ\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[HokaninanikaState alloc] init]; } else if(inputValue == 2) { return [[BukiSelectState alloc] init]; } return self; } @end // 『兜でよろしいですね?』状態 @implementation KabutoState - (id) update { int inputValue; printf("兜でよろしいですね?\n"); printf("1. はい 2. いいえ\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[HokaninanikaState alloc] init]; } else if(inputValue == 2) { return [[BukiSelectState alloc] init]; } return self; } @end // 『本当に売りますか?』状態 @implementation UrimasukaState - (id) update { int inputValue; printf("本当に売りますか?\n"); printf("1. はい 2. いいえ\n"); scanf("%d", &inputValue); // ここ別に処理を分けなくてよかったね。 if(inputValue == 1) { return [[HokaninanikaState alloc] init]; } else if(inputValue == 2) { return [[HokaninanikaState alloc] init]; } return self; } @end // 『ほかに御用はありますか?』状態 @implementation HokaninanikaState : NSObject {} - (id) update { int inputValue; printf("ほかに御用はありますか?\n"); printf("1. はい 2. いいえ\n"); scanf("%d", &inputValue); if(inputValue == 1) { return [[IrasshaiState alloc] init]; } else if(inputValue == 2) { return [[ArigatouState alloc] init]; } return self; } @end // 『ありがとうございました』状態 @implementation ArigatouState - (id) update { printf("ありがとうございました\n"); printf("またのご来店をお待ちしております\n"); return Nil; } @end // =============================================== // main 関数 // =============================================== int main() { id state = [[IrasshaiState alloc] init]; id next; // 次の遷移状態 while((next = [state update]) != Nil) { if(state != next) { [state release]; state = next; } } }
実行すると、こんな感じでちゃんと遷移できている事が判る。やったね!地味だけど。
ちなみに今日のお話は、GoF のデザインパターンのうち、『State パターン』と呼ばれているものである。
ゲームの画面遷移などで有効なテクニックだ(とぼくは信じている)。
あと、『遷移するたびにメモリを alloc するのってどうよ?』と思われるかも知れない。alloc は非常にオーバヘッドの大きな処理なので、それなりに考慮すべき事なのだが、これを考え始めると頭が痛くなるので今日はここまで。