リスト5に、4つの状態クラスを示します。各クラスは小さくて単純なので、直接簡単にテストできます。
// CheckedOut.java
import java.util.*;
class CheckedOut extends HoldingState {
CheckedOut(Holding holding) {
super(holding);
}
@Override
public void checkin(Date date) {
holding.doCheckin(date);
holding.state = new CheckedIn(holding);
}
@Override
public void placeHold(Date date, String patronId) {
holding.doHold(date, patronId);
holding.state = new CheckedOutHeld(holding);
}
}
// CheckedIn.java
import java.util.*;
class CheckedIn extends HoldingState {
CheckedIn(Holding holding) {
super(holding);
}
@Override
public void checkout(Date date, String patronId) {
holding.doCheckout(date, patronId);
holding.state = new CheckedOut(holding);
}
@Override
public void placeHold(Date date, String patronId) {
holding.doHold(date, patronId);
holding.state = new CheckedInHeld(holding);
}
}
// CheckedInHeld.java
import java.util.*;
public class CheckedInHeld extends CheckedIn {
CheckedInHeld(Holding holding) {
super(holding);
}
@Override
public void checkout(Date date, String patronId) {
if (patronId != holding.holdPatron)
throw new HoldException();
holding.doCheckout(date, patronId);
holding.state = new CheckedOut(holding);
}
@Override
public void placeHold(Date date, String patronId) {
throw new HoldException();
}
@Override
public void update(Date date) {
holding.doReleaseOldHold(date);
}
}
// CheckedOutHeld.java
import java.util.*;
public class CheckedOutHeld extends HoldingState {
CheckedOutHeld(Holding holding) {
super(holding);
}
@Override
public void checkin(Date date) {
holding.doCheckin(date);
holding.state = new CheckedInHeld(holding);
}
@Override
public void placeHold(Date date, String patronId) {
throw new HoldException();
}
}
できあがったHoldingクラス(リスト6を参照)には、条件ロジックがほとんどありません。テストも非常に簡単です。
Stateパターンは、クラス間の密接な結び付きを必要とする数少ないデザインパターンの1つです。Holdingクラスは初期状態に依存し、そして次に各状態がHoldingに依存します。これを断ち切るための優れた方法もいくつかありますが、ほとんどの場合、そうする必要はありません。なぜなら、クラスは自己閉鎖的なサブシステムであり、システムの他の部分から切り離されているからです。今回の状態クラスの実装では、Javaのパッケージレベルアクセスを利用しました。これにより、状態オブジェクトは必要に応じてHolding変数に直接アクセスできますが、これらのフィールドはHoldingの外部クライアントに公開されません。フィールドはプライベートにすべしというのが普段の私の主張なのですが、これは数少ない例外の1つです。
import java.util.*;
public class Holding {
private final Book book;
private final int copyNumber;
Date checkoutDate;
String holdPatron;
Date holdDate;
HoldingState state = new CheckedIn(this);
Date checkinDate;
public Holding(Book book, int copyNumber) {
this.book = book;
this.copyNumber = copyNumber;
}
public Book getBook() {
return book;
}
public int getCopyNumber() {
return copyNumber;
}
public boolean isOnLoan() {
return checkoutDate != null;
}
public Date getLoanDate() {
return checkoutDate;
}
public void checkout(Date date, String patronId) {
state.checkout(date, patronId);
}
public void checkin(Date date) {
state.checkin(date);
}
public void placeHold(Date date, String patronId) {
state.placeHold(date, patronId);
}
public void update(Date date) {
state.update(date);
}
public boolean isOnHold() {
return holdPatron != null;
}
public void releaseAnyHold() {
holdPatron = null;
}
// callback actions
void doHold(Date date, String patronId) {
holdPatron = patronId;
holdDate = date;
}
void doCheckout(Date date, String patronId) {
checkoutDate = date;
releaseAnyHold();
}
public void doCheckin(Date date) {
checkoutDate = null;
}
public void doReleaseOldHold(Date updatedAt) {
if (DateUtil.daysBetween(holdDate, updatedAt) >= 3)
releaseAnyHold();
}
}
一般に、状態遷移図に新機能を追加するのは簡単です。状態クラスが個別化されていることで、発生条件の複雑さが回避され、条件ロジックが最小限で済みます。場合によっては、変更によって状態間の遷移に変化が生じ、いくつかの状態派生クラスを更新しなければならないこともあります。状態遷移図を描くことは有益な手法ですが、遷移の管理に役立つ方法は他にもあります。
状態遷移図は単純なテーブルとして表現できます。もっと厳密なStateパターンへとリファクタリングしていけば、作業を劇的に簡単化できるはずです。テーブルを使う場合は、イベントメソッドとコールバックアクションメソッドの名前をテーブルに定義します。これをそのまま、状態派生クラスのコードの自動作成に利用できるでしょう。非常に動的な状態システムでは、このような自動化は重要です。Object MentorのWebサイトから、このようなJavaコードジェネレータであるSMC(State Machine Compiler)をダウンロードできます。また、より動的なサブシステムで状態システムの変更を簡単化できるStateパターンのバリエーションも紹介されています。

