まとめ
- 「かわいそうなGoF Stateパターンに愛の手を」この記事読んだけど CoffeeScript がわからん
- とりあえず State パターンがとても便利そうなのはわかった
- Wikipedia の State パターンを読んだら実装例が Java だった
- Java よくわかんねーよ! ということで C++ でそのまま書き直してみた
- だいたいイメージはつかめたけど C++ ならもっとスマートな実装ありそう
State パターンて何よ
オブジェクト指向が持つと言われている特性のひとつ、ポリモルフィズムを発揮できるわざ。
こうか:Switch 文を無くす事ができる。
オブジェクト指向を勉強している身としては、ネタの引き出しにどれだけマスターしたデザインパターンがあるかで設計や実装の速さが変わってくるように思う。
UML 図を見る限り、概念そのものはわりと難しくなさそう。
ならば自分が知っているオブジェクト指向言語でも再実装してみれば、何かが分かるはず。
そんなわけで、実装してみた。
State パターンの実装例
動作を簡単に説明する。 Wikipedia を参考にしたけど、 C++ で文字列処理は面倒くさすぎるので一部をアレンジした。
- 引数の文字列をそのまま返す
- 引数の文字列の末尾にプラス記号 (+) をつけて返す
- 引数の文字列の末尾にプラス記号 (+) をつけて返す
- 引数の文字列をそのまま返す
- 引数の文字列の末尾にプラス記号 (+) をつけて返す
- 引数の文字列の末尾にプラス記号 (+) をつけて返す
この動作をずっと繰り返し続ける。深く考えずに書くと、状態を保存して、カウントして、 switch して・・・となりがちだが、 State パターンだとすっきりかけるらしい。
以下、実装例。
/*
* C++ で State パターンの練習
* Wikipedia の State パターン (Java) を C++ 風に直してみた
* ヘッダ版
*/
#include <string>
// 前方宣言
class StateContext;
// 状態のための抽象クラス
class State{
public:
virtual ~State() {
}
virtual void WriteString(StateContext *state_context, const std::string &str) = 0;
};
// 状態クラスその1
class StateBasic : public State{
public:
void WriteString(StateContext *state_context, const std::string &str);
};
// 状態クラスその2
class StatePlus : public State{
public:
StatePlus(void): cnt() {
}
void WriteString(StateContext *state_context, const std::string &str);
private:
int cnt;
};
// 文脈クラス
class StateContext{
public:
StateContext(void): mState(new StateBasic()), mNext() {
}
// 次の状態を準備
void SetNextState(State *state) {
if (mNext) delete mNext;
mNext = state;
}
// 状態ごとに異なる動作を行う
void WriteString(const std::string &str) {
mState->WriteString(this, str);
StateToForward();
}
private:
State *mState;
State *mNext;
// 状態を進めてみる
void StateToForward(void) {
if (!mNext) return;
if (mState) delete mState;
mState = mNext;
mNext = 0;
return;
}
};
/*
* C++ で State パターンの練習
* Wikipedia の State パターン (Java) を C++ 風に直してみた
* 実装版
*/
#include <iostream>
#include "StatePattern.h"
// 状態その1 文字列をそのまま出力
void StateBasic::WriteString(StateContext *state_context, const std::string &str) {
std::cout << str << std::endl;
state_context->SetNextState(new StatePlus());
}
// 状態その2 文字列にプラスを追加して出力
void StatePlus::WriteString(StateContext *state_context, const std::string &str) {
std::cout << str << "+" << std::endl;
if (++cnt >1)
state_context->SetNextState(new StateBasic());
}
// エントリポイント
int main(void){
StateContext state_context;
state_context.WriteString("aaa");
state_context.WriteString("bbb");
state_context.WriteString("ccc");
state_context.WriteString("ddd");
state_context.WriteString("eee");
state_context.WriteString("fff");
return 0;
}
実行した結果はこちら。
$ ./a.out
aaa
bbb+
ccc+
ddd
eee+
fff+
ちゃんと状態が呼び出すごとに状態が変化している。
ちゃんと調べたわけじゃないけど、メモリリークもしていないはず。・・・たぶん。
気づいたことなどいろいろ
Java 版と一番の違いはメモリ管理を自分でしないといけないことだっだ。 new と delete が面倒というか泥臭い書き方になっている。ここは正直どうにかしたかった。 C++ なら C++ なりのイディオムがありそうな気はする。
状態を遷移させるのにいちいち new しなきゃいけないことが若干気になる。速度面でも、メモリフラグメンテーションでも。このままの実装だと大きなクラスで使えなさそう。ただ、本当に速度を気にしなきゃいけないほど State クラスが大きいなら、そもそももっと別のデザインパターンを選んだ方が良い気がする。というか new が嫌なら C で関数ポインタテーブル書けって話になのかも。ここら辺、ゲームプログラミング向けの本とか読めば良いのかな。ゲーム業界は脱初心者向けな書籍が多くていいですね。
今回、自分で実装してみて、 State パターンのキモが何となく分かった。
- 状態クラスは共通の親クラスを継承してつくること
- 文脈クラスが状態クラスを呼び出し、文脈クラスは次に呼び出すべき状態を知っていること
- クライアントコードは状態クラスを直接触らず、文脈クラスを経由すること
これまで、状態クラスと文脈クラスを分ける、という発想がなかなか理解できなくて悩んでいたように思う。クラスの関係性が2段、3段になってくると考え方が抽象的すぎて頭がなかなかついていかない・・・。
まあ、これから慣れていきましょう、ということで。