今回からは、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パターンによる状態と振る舞いのカプセル化だ。状態管理が複雑になればなるほど、その威力を発揮するだろう。