今回からは、Stateパターンを取り上げよう。Stateパターンは、その名の通り「状態」を表すためのパターンだ。従来、状態は状態変数と呼ばれる変数で管理されてきた。これを、オブジェクト化して、振る舞いの記述を別クラスに切り出してやろう、というのがStateパターンの考え方だ。
Stateとは
プログラムでは、その時々の状態によって、振る舞いを変える必要がある事が多い。いや、状態に対する振る舞いの記述こそ、プログラミングそのものと言ってもいいかもしれない。このような抽象的な言を弄していても仕方ないので、具体的な例を挙げよう。
取り上げるのは、GoF本で紹介されている、TCPの接続プロトコルだ。TCP接続の仕様では、それぞれのホストでの接続の「状態」が定義されている。たとえば、接続していないClosed状態、接続中であるEstablished状態、相手のレスポンスを待っているListen状態、などだ。それぞれの状態に対して、行える振る舞いが決まっている。たとえば、Closed状態からは、接続をオープンする事が出来る。すると、Established状態に遷移するのだ。Established状態からはデータの転送を行う事が出来て、Listen状態に遷移する。
このように、プログラムの状態と、その振る舞いは、深い関係がある。多くのプログラムは、内部の状態を遷移させて、適切な振る舞いを行う事で動作しているのだ。このような、状態、振る舞い、および状態の遷移を表すためのツールとして、状態遷移図と呼ばれるものが使われる。また、状態をもつ対象を、オートマトンとも呼ぶ。状態遷移やオートマトンをどのように記述するかは、情報科学の分野で確立された理論がいくつかある。
話が横道にそれたので、もとに戻そう。TCP接続の状態を表すには、列挙型の変数を用いるのが最も簡単だろう。つまり、次のような定数を用意する。
List 1.
enum {
TCPClosed,
TCPEstablished,
TCPListen,
};
そして、TCP接続を行うTCPConnectionクラスを作り、この状態を表す変数を1つ持たせるのだ。このクラスの振る舞いは、変数の値によって決定する事になる。たとえば、接続を行うメソッドactiveOpenメソッドは、次のようになるだろう。
List 2.
- (void)activeOpen
{
switch (_state) {
case TCPClosed: {
// 接続処理を行う。
...
break;
}
case TCPEstablisehd:
case TCPListen: {
// この状態のときは何もしない。
break;
}
}
}
これが、状態による振る舞いの変化という事になる。
このソースコードは、これはこれで問題はない。だが、条件分岐があるのが気になるかもしれない。この方式だと、すべての振る舞いの中で、この種の条件分岐を書かなくてはいけないだろう。もし、状態の数が増えるような事があったら、すべての条件分岐を修正しなくてはいけない。これはやっかいだ。
そこで、この状態と振る舞いを、1つのクラスにまとめてしまおう、というのがStateパターンの考え方だ。これにより、状態遷移をカプセル化していまい、保守しやすくするのだ。
Stateパターンの登場人物
状態をクラス化するパターンには、どのようなクラスが必要になるかを考えてみよう。
まずもちろん、状態を表すクラスが必要だ。これをStateクラスとしよう。Stateクラスから、サブクラスを作る。このサブクラスが、それぞれの状態を表すのだ。先ほどのTCP接続の例であれば、TCPClosedを表すサブクラス、TCPEstablishedを表すサブクラス、などを作る事になる。
Stateクラスには、振る舞いを記述する事になる。まず、親クラスであるStateクラスに、すべての可能な振る舞いを、メソッドとして実装する。そしてサブクラスでは、それぞれの状態に応じた振る舞いを、メソッドをオーバーライドすることで記述するのだ。
次に、Stateクラスを保持するクラスが必要だ。これをContextクラスと呼ぼう。Contextクラスでは、Stateクラスのインスタンスを1つ持つものとする。これが、このプログラムの現在の状態を表す事になる。
Contextクラスが何か振る舞いを行いたいときは、単にStateクラスのメソッドを呼び出せばいい。ここで重要なのは、Contextクラスで現在の状態が何であるかという条件分岐を行わない事、そして実際の振る舞いはContextクラスには実装されていない事、である。これらは、Stateクラスにゆだねられる事になる。これが、状態と振る舞いのカプセル化ということだ。
さらにContextクラスには、状態を遷移させるメソッドも必要になる。これは、Stateクラスのインスタンスを入れ替える事で実現出来るだろう。
これらのクラスを図示すると、次のようになる。
Stateパターンの実装
では、Stateパターンの実装例を示そう。実際に作ってみるのは、最初に紹介したTCP接続である。
まず、TCP接続のためのクラスとして、TCPConnectionを定義する。これは、StateパターンでいうところのContextとなるクラスである。同時に、Stateとなるクラスも宣言しておこう。これは、TCPStateというクラスにする。
List 3.
@interface TCPConnection : NSObject
{
TCPState* _state;
}
- (void)activeOpen;
- (void)passiveOpen;
- (void)close;
- (void)send;
- (void)acknowledge;
- (void)synchronize;
- (void)chnageState:(TCPState*)state;
@end
TCPConnectionクラスは、インスタンス変数としてTCPStateのオブジェクトを持つ。これが、現在の状態を表す訳だ。そして、接続を制御するためのメソッドを用意する。さらに、状態を遷移させるためのメソッドも用意した。
このクラスの初期化処理では、最初の状態を設定する。初期状態は、接続が閉じられた状態なので、これをTCPClosedというクラスで表そう。
List 4.
@implementation TCPConnection
- (id)init
{
self = [super init];
if (!self) {
return nil;
}
// 初期状態を設定する。
[self changeState:[TCPClosed sharedInstance]];
return self;
}
それぞれの接続処理を行うメソッドでは、具体的な事は何もやっていない。すべて、Stateクラスに任せてしまうのだ。たとえば、activeOpenメソッドでは、TCPStateクラスのactiveOpenWithConnection:を呼び出すだけである。
List 5.
- (void)activeOpen
{
// Stateクラスのメソッドを呼び出す。
[_state activeOpenWithConnection:self];
}
...
状態遷移のためのメソッドも見ておこう。ここでは、単に_state変数を入れ替えるだけである。状態遷移の判断と、そのためのTCPStateインスタンスの取得も、TCPState側に任せてしまっているのだ。
List 6.
- (void)chnageState:(TCPState*)state
{
// 状態遷移を行う。
[_state release];
_state = [state retain];
}
次に、TCPStateクラスを紹介しよう。TCPStateクラスのサブクラスは、インスタンスを1つしか生成しないSingletonとしよう。そのために、sharedInstanceというクラスメソッドを用意する。その他には、振る舞いのためのメソッドを用意しよう。
List 7.
@interface TCPState : NSObject
{
}
+ (id)sharedInstance;
- (void)activeOpenWithConnection:(TCPConnection*)connection;
- (void)passiveOpenWithConnection:(TCPConnection*)connection;
- (void)closeWithConnection:(TCPConnection*)connection;
- (void)sendWithConnection:(TCPConnection*)connection;
- (void)acknowledgeWithConnection:(TCPConnection*)connection;
- (void)synchronizeWithConnection:(TCPConnection*)connection;
@end
TCPStateのサブクラスとして、TPClosed、TCPEstablished、TCPListenを用意する。それぞれのクラスで、必要なメソッドを上書きすればいい。
TCPClosedで上書きするのは、接続を開始するためのメソッドになるだろう。つまり、activeOpenWithConnection:と、passiveOpenWithConnection:だ。このメソッドの中で、具体的な接続処理を行う。そして、ここがポイントだが、接続処理が終わったら状態をEstablishedに遷移させるのだ。これは、TCPConnectionのchangeState:メソッドを呼び出す事で行う。
List 8.
@implementation TCPClosed
- (void)activeOpenWithConnection:(TCPConnection*)connection
{
// 接続処理を行う。
...
// 状態をEstablishedに遷移する。
[connection chnageState:[TCPEstablished sharedInstance]];
}
- (void)passiveOpenWithConnection:(TCPConnection*)connection
{
// 接続処理を行う。
...
// 状態をEstablishedに遷移する。
[connection chnageState:[TCPEstablished sharedInstance]];
}
...
このような感じで、他のメソッドや他のクラスを実装していけばいい。
これが、Stateパターンによる状態と振る舞いのカプセル化だ。状態管理が複雑になればなるほど、その威力を発揮するだろう。