Stateパターンのおさらい
さて、今回やんなきゃいけないのは、テキストファイルを読み出してfieldごと/recordごとに文字列を切り出すことです。面倒なのはこの切り出し条件がややこしいこと。「カンマが来たらfieldの区切り/改行が来たらrecordの区切り」なんて単純な条件だけなら何のことはないのですが「escape状態ではカンマはfieldの区切りではない」とか「escape状態で二重引用符が来たからといってそれがescape状態の終わりとは限らない」とか、読み出した文字に応じた処理が状態によって変化しますからね。
CSV読み出しアプリケーションを見通しよく実装するため、デザイン・パターンの一つ: Stateパターンを用いることにしました。Stateパターンのサンプルとして机の上に転がっている「ノック式ボールペン」をプログラムで表現してみましょう。ノック式ボールペンには2つの状態、「書けない(disabled)状態」と「書ける(enabled)状態」がありますね。この2つの状態は「ノック(knock)された」という事象(イベント)によって交互に切り替わります。その様子を図示するとこんな感じ:
この図は状態が遷移する様子を表した図なので「状態遷移図」と呼ばれます("まんま"やね)。それぞれの丸が状態、矢印が状態の遷移、その遷移を引き起こす事象(イベント)とそのときに行う活動(アクション)が記されています。
まずはStateパターンを用いないフツーの実装:状態disabled/enabledをenumで表現すれば…
#include <string>
using namespace std;
/*
* Context: ペンの動作(アクション)を司る
*/
class Context {
public:
virtual ~Context() {}
virtual void write(const string& text) =0;
};
class Pen {
Context* context_;
enum { disabled, enabled } state_;
public:
explicit Pen(Context* context) : context_(context), state_(disabled) {}
void knock() {
switch ( state_ ) {
case disabled: state_ = enabled; break;
case enabled: state_ = disabled; break;
}
}
void write(const std::string& str) {
switch ( state_ ) {
case disabled: break;
case enabled: context_->write(str); break;
}
}
};
/*
* おためし
*/
#include <iostream>
// 標準出力(cout)に書くContext
class StandardContext : public Context {
public:
virtual void write(const string& text) {
cout << text;
}
};
int main() {
StandardContext ctx;
Pen pen(&ctx);
pen.write("はじめは書けないけど");
pen.knock();
pen.write("一度ノックすれば書けます。");
pen.knock();
pen.write("も一度ノックすると書けなくなります。");
pen.knock();
pen.write("ノック式ボールペンてばそんなもの。");
}
…ざっくりこんな感じでしょうか。状態がdisabled/enabledの2つだけだし、起こる事象もknockだけなので単純なコードで実装できます。実行結果は以下のとおり。
では次にStateパターンを用いた実装を示します。
まず、状態を表現するenumをclass Stateに改めます。Stateのメソッドは状態の変化を引き起こす事象に反応するハンドラ、および状態に応じて挙動の異なるものを並べます。class Stateは状態:disabled/enabledを表現するclassState_disabled/State_enabledの基底クラスとなります。
// Contextは変わらず...
class State {
friend class Pen;
protected:
static State* disabled();
static State* enabled();
virtual State* knock(Context* ctx) =0;
virtual State* write(const string& text, Context* ctx) =0;
};
このclass Stateから、2つの状態を表す State_disabled/State_enabledを導出します:
// 書けない状態
class State_disabled : public State {
// knockされたら書ける状態に。
virtual State* knock(Context* ctx) {
return enabled();
}
// writeされても何もせず、書けない状態のまま。
virtual State* write(const string& text, Context* ctx) {
return disabled();
}
};
// 書ける状態
class State_enabled : public State {
// knockされたら書けない状態に。
virtual State* knock(Context* ctx) {
return disabled();
}
// writeされたら書き、書ける状態のまま。
virtual State* write(const string& text, Context* ctx) {
ctx->write(text);
return enabled();
}
};
State* State::enabled() {
static State_enabled state;
return &state;
}
State* State::disabled() {
static State_disabled state;
return &state;
}
Stateおよびその導出クラスには一切のメンバ変数を持たせないのがキモです。「そうしなければならない」ということはないのですが、メンバ変数を持つということはそのオブジェクトが状態を持つことに他なりません。状態を表現するオブジェクトが状態を持ってたらヤヤコシイことになりかねませんから。挙動にかかわる変数やメソッドはすべてContext側に置き、Stateから呼び出す形にしておきます。
ノック式ボールペンの本体であるPenはState_disabledもしくはState_enabledのいずれか保持するState* state_を内包し、状態の遷移を引き起こすもしくは状態に応じて挙動の異なるメソッドはすべてstate_に委譲します:
/*
* Pen: ペン本体 上記ContextとStateを内包する
*/
class Pen {
State* state_;
Context* context_;
public:
Pen(Context* context) {
state_ = State::disabled();
context_ = context;
}
// 現状態でknockし、新たな状態へ遷移。
void knock() {
state_ = state_->knock(context_);
}
// 現状態でwriteし、新たな状態へ遷移。
void write(const string& text) {
state_ = state_->write(text, context_);
}
};
// おためしコードも変わらず...


