状態の追跡
取り置き本(Holding)はさまざまな状態を取ると考えられ、例えば貸し出し中、返却済み、取り置き依頼あり、取り置き依頼なし、分館から分館への移動中などがあります。既存のコードの場合、Holdingは次の4つの状態のいずれかです。
- 貸し出し中、取り置き依頼あり
- 貸し出し中、取り置き依頼なし
- 返却済み、取り置き依頼あり
- 返却済み、取り置き依頼なし
状態を定義することで、状態遷移図を作成できます(図1を参照)。状態遷移図では、個々の状態をボックスとして表現します。Holdingの返却など、特定のイベントが発生すると、Holdingは状態間を遷移します。通常は、別の状態に遷移する中でイベントによってアクションが呼び出されます。

Stateパターンでは、こうした個々の状態を切り出し、それぞれを独立したクラスとして表現します。現在の状態は、Holdingオブジェクト内で、状態オブジェクトへの参照として追跡されます。今回の例では、起こり得るイベントをすべての状態に対するメソッドとして定義します。ただし各状態では、Holdingオブジェクトでアクションを呼び出すことにより、それぞれに適したイベントセットが処理されるようにしています。状態の遷移が必要な場合は、イベントハンドラでは、Holdingオブジェクトが持つ状態参照の更新も行います。
テストを繰り返しながら、時間をかけてコードのリファクタリングをしていきましょう。作成する最初のクラスはHoldingStateクラスです(リスト4を参照)。これは、起こり得る各イベントに対してNULL動作を提供する抽象クラスです。
// HoldingState
import java.util.*;
abstract class HoldingState {
protected final Holding holding;
HoldingState(Holding holding) {
this.holding = holding;
}
void checkout(Date date, String patronId) {
}
void checkin(Date date) {
}
void placeHold(Date date, String patronId) {
}
void update(Date date) {
}
}
次に、このクラスに基づいて、4つの状態にそれぞれ対応する派生クラスを作成します。派生クラスを作成したら、Holdingクラスからコードを移動させます。各イベント(checkout、checkin、placeHold、update)に対するHoldingオブジェクトの役割は、そのイベントを現在のHoldingStateオブジェクトにそのままデリゲートすることです。例えば、Holdingのcheckoutメソッドは、最終的には次のような形になります。
public void checkout(Date date, String patronId) {
state.checkout(date, patronId);
}
これらは単純なデリゲートなので、Javaプロキシのメカニズムを使うことにしましょう。現時点では、新たにHoldingに実装する必要が出てきた数多くのpublicインターフェースについては考慮しません。Holdingの拡張については後で行います。
checkoutイベントに対して何らかの処理を行う必要がある状態オブジェクトは、アクションメソッドの呼び出しか、別の状態への遷移を行います(両方行う場合もあります)。状態オブジェクトの種類によっては、このイベントを無視することもあります。Stateパターンを実装すると、無視されていたイベントが見つかり、システムの潜在的な問題が明らかになることがあります。例えば、現時点のCheckedOut状態オブジェクトは、checkoutイベントに対して何も動作しません。この処理されないイベントのおかげで、誰かが同じ本を2回借りようとしたときにどう対処すべきかを決めなければならないことに気付きました。
すべてのコードを状態オブジェクトに移動させる利点は、条件分岐が減ることです。CheckedInHeld状態のときに、処理を進める前に本が取り置きされているかどうかを確認する必要はもうありません。
class CheckedInHeld extends HoldingState {
// ...
@Override
public void checkout(Date date, String patronId) {
if (patronId != holding.holdPatron)
throw new HoldException();
holding.doCheckout(date, patronId);
holding.state = new CheckedOut(holding);
}
// ...
}
以前は、Holding内のcheckoutのコードで、本が既に取り置きされているかどうかを確認しなければなりませんでした。しかしコードを移動させたことにより、HoldingがCheckedInHeld状態を参照するときにこのコードが実行されるので、もう確認は必要ありません。
