貯金箱プログラムのリファクタリング

今週は,オブジェクト指向から少し離れて,プログラムの「リファクタリング」をテーマにします.

リファクタリング」とは,プログラムの機能を変えないで中身を改善していく作業のことです.前回までに作成した貯金箱プログラムをよく見てみると,たとえば,以下のような問題を指摘することができます.

(1) 同じような処理をするコードが散らばっている
SavingsBoxクラスのinsertCoin100とinsertCoin500は,ほとんど同じ中身で,違っているのはその金額だけです.これは通称コピペコードと呼ばれ,プログラマの間で忌み嫌われるものです.また,コインの種類を増やそうとしたとき,いちいちinsertCoin1, insertCoin5 などといった似たようなメソッドをさらに追加しなくてはなりません.これを何とかしましょう.

(2) 鍵がかかっている貯金箱に対して鍵がかけられる
これは,どちらかというとプログラムの仕様(機能)に関することかもしれませんが,前回のプログラムでは鍵がかかっている貯金箱にさらに鍵をかけることができてしまいます(事実上は何もしませんが).これは気持ちが悪いので,これらの操作に対してチェックを行い,鍵が既にかかっている貯金箱には改めて鍵をかけられないようにしましょう.

(3) コマンドの入力方式が拡張性に欠ける
貯金箱と操作,コインの種類の組み合わせに対して,コマンド番号を割り当てているので,これらの数や種類が増えると,コマンド番号が増えて破綻します.もう少し一般的な入力方式を考えることにしましょう.

ところで,リファクタリングというのは,既に動いているソフトウェアを改変するわけですから,それによってもともと動いていた機能が動かなくなる,新たにバグを入れ込んでしまう(デバッグの反対なのでエンバグといったりします),というような危険性があります.これは多くの人に使ってもらうような種類のプログラムでは大問題(おそらくプログラムの作者に対してユーザから文句のメールが殺到することでしょう)になります.そのような事態を避けるためには,テストが重要です.リファクタリング対象となるプログラムのテストコードを書いて,リファクタリングした後でそのテストコードを実行し,機能が損なわれていないことを確認するのが良いと思います.(今回は省略します...)

列挙型を使ってコインの種類を表す

まずは,問題点(1)についてよく考えてみます.コインの種類を増やしたときに,貯金箱が提供すべき操作(メソッド)の種類が増えてしまうのは,おかしなことです.あくまでも「コインを挿入する」という操作はコインの種類と無関係になるようにしましょう.

そこで,insertCoin100, insertCoin500というようにメソッド名でコインの種類を表現するのではなく,insertCoinメソッドの引数でコインの種類を指定するようにします.さて,引数の型はどうするのがよいでしょうか?一番簡単なのは,コインの額面を引数にしてしまうことです.たとえば,100円玉を一枚挿入する場合,insertCoin(100), 500円玉の場合はinsertCoin(500)というようにする方法です.この方法は簡単で良いのですが,123円玉や-500円玉のようなあり得ないコインも貯金できてしまうので,今回の貯金箱のモデルとしては好ましくありません.もちろん,引数をチェックすることによって,へんてこなコインの挿入を排除することは可能ですが,それよりも,コインの種類を適切に表現できるような何かを定義しましょう.

そのような表現にぴったりのJavaの言語機構として,ここでは,列挙型を使った方法を紹介します.列挙型はたとえば,曜日(月,火,水,...)とか色(赤,青,黄,...)など,有限の種類のデータ型を表現するのに適しています.ここでは,コインの種類,1円玉,5円玉,10円玉,...を列挙型Coinとして定義することにしましょう.Javaで列挙型を定義したい場合,enumキーワードを使って以下のように書くことができます.

enum Coin { Y1, Y5, Y10, Y50, Y100, Y500 }

これで,Coin型が定義できました.50円玉を挿入する場合は,insertCoin(Coin.Y50)のようにします.

さてここで,ひとつ問題があります.コインの額面の情報をどうやって持たせたらよいのでしょうか?Coin型の要素(Y1, Y5, ...)と額面を対応させる表を別途定義する,というのも一案ですが,ここではもっと直接的に,Coin型の列挙定義の中にその情報を埋め込んでしまいましょう.また,額面を取得するメソッドvalue()も同時に定義してやります.

enum Coin {
    // コインの種類を定義(額面で初期化)
    Y1(1), Y5(5), Y10(10), Y50(50), Y100(100), Y500(500);
    private final int value; // 額面情報を保持するフィールド
    Coin(int value) { this.value = value; }
    public int value() { return value; }
}

これにあわせて貯金箱の定義を以下のように修正します.

class SavingsBox {
    ...
    private ArrayList<Coin> coins; // 貯金箱にあるコイン達
    ...
    // コインを入れる
    public void insertCoin(Coin c) { coins.add(c); }
    ...
    // 合計金額を取得する
    public int getAmount() {
        int total = 0;
        for (Coin c : coins) {
            total += c.value();
        }
        return total;
    }
    ...
}

insertCoin()では,コインをArrayListに追加するだけの処理とし,getAmount()が呼ばれるたびに,コインの合計金額を計算するようにしました.
※ ここでは,insertCoin()メソッドの中では,鍵がかかっているかどうかのチェックはしていません.これは以降の説明で修正します.

貯金箱の施錠状態をチェックする

前回の貯金箱では,コインの挿入時には,鍵の状態をチェックしていましたが,鍵をかけるとき(lock)と,はずすとき(unlock)にもこれらのチェックを行うように修正しましょう.

要件を整理すると,

  • insertCoin操作は解放(locked==false)状態のときのみ許される
  • lock操作は解放(locked==false)状態のときのみ許される
  • unlock操作は施錠(locked==true)状態のときのみ許される

ということになります.

鍵かかかった状態でのlock操作や鍵がはずれた状態でのunlock操作は不正な操作ということで,その都度チェックする必要があります.さて,insertCoinと同様に,このチェック処理を貯金箱クラス(SavingsBox)に押し付けてしまい,lock 操作, unlock操作の中で施錠状態をチェックしてもよいのですが,今回はその責任を呼び出し側にまかせることにしましょう.すなわち,これらの操作を呼び出す側で,もし貯金箱の施錠状態がわからない場合には,

if (savingsBox.isLocked() == false) {
   savingsBox.insertCoin(Coin.Y100);
}

のように,必ず貯金箱の状態を確認してから呼び出すようにします.

このようにすることのメリットは何でしょうか?lockやunlock操作の中でいちいちチェックする必要が省けるのでSavingsBoxクラスの実装はシンプルになりますが,その分,呼び出し側のチェックが必要になってくるので,その点ではあまり変わり嬉しいことはありません.一番のメリットは,エラー処理を呼び出し側に任せることで,貯金箱クラスの再利用性が高まることです.前に作った貯金箱クラスのinsertCoin100()の処理を見てください.このメソッドの中では,以下のように鍵がかかった状態のときにはエラーメッセージを表示して終了するようにしています.

public void insertCoin100(int coins) {
    if(this.lock) {
        // コインは挿入できない。メッセージにその旨が表示される。
        this.showMessage("この貯金箱には鍵がかかっています。");
    } else {
    ...

このSavingsBoxクラスを別のアプリケーションの中で再利用することを考えたとき,このままではエラー発生時の扱いが固定されてしまいます.アプリケーションによっては,エラー時にはメッセージをログファイルに書き出したい,とか,あるいはネットワークアプリケーションのような場合,エラーが発生したことをメッセージとして要求元に応答したい,といったケースがあるかもしれません.そのような場合,SavingsBoxクラスの中のエラー処理の部分をいちいち書き換えなくてはならなくなり,再利用性を損ねます.ということで,今回は,呼び出し側(アプリケーション側)でエラーのチェックをし,必要に応じてエラー処理を行うことにします.これは,SavingsBoxクラスの利用者に対して,「insertCoin()はunlock状態の時にのみ呼び出し可能である」という契約を結ぶことを意味します.利用者はこの契約を守る義務があります.また,操作を提供する側は,この契約が守られることを前提にinsertCoin()を実装することができるので,以下のようにシンプルに記述することができるようになります.

public void insertCoin(Coin c) { coins.add(c); }

これでも良いのですが,利用者側のコードにバグがあり,契約が破られてinsertCoinを呼び出された場合,何ごともなかったかのように,処理が進んでしまいます.そこで,表明(asseert)を用いてそのようなケースをきちんと発見できるようにしましょう.

Javaのassert文を使うと,その文の実行時に満たされているべき条件を明確に記述することができるようになります.ここでは,insertCoinメソッドを実行する前に満たされているべき条件(事前条件ともいいます)locked==falseを,assert文を使って明示的に指定するよう,insertCoinメソッドを以下のように書き直します.

public void insertCoin(Coin c) {
    assert this.locked == false;
    coins.add(c);
}

assertキーワードの後には,boolean型の値を返す式を記述します.実行時に,この式が偽(false)と評価されると,エラー(AssertionError)が報告されてプログラムが停止します.真(true)と評価された場合には,assert文は何事もなかったかのように終了し,その次の文が実行されます.

上のassert this.locked == falseの例では,貯金箱に鍵がかかっていなければ,何ごともなかったかのように処理が続くけれど,鍵がかかっている場合(this.locked == falseの結果が偽,すなわちthis.lockedがtrueの場合),AssertionErrorが発生し,プログラムのどの部分でassertが失敗したかを報告し,プログラムが終了します.このようにしておくことで,プログラムにバグがあって契約が満たされなかった場合,プログラマはどこに原因があるのかすぐに見つけることができます.
#ちなみに,このような考え方にもとづくプログラムの設計を「契約による設計#(Design by Contract:DbC)」と呼びます.DbCを直接的にサポートしているオブジェクト指向プログラミング言語としてEiffelが有名です.Eiffelでは事前条件,事後条件をプログラムに記述することが可能です.

以上をまとめると,最終的にSavingsBoxクラスの定義は以下のようになります.

public class SavingsBox {
    private String name;
    private ArrayList<Coin> coins;
    private boolean locked;

    public SavingsBox(String name) {
        this.name = name;
        this.coins = new ArrayList<Coin>();
        this.locked = false;
    }

    public void insertCoin(Coin c) {
        assert !locked; // 鍵がかかっていないこと
        coins.add(c);
    }
    public String getName() { return name; }
    public boolean isLocked() { return locked; }

    public int getAmount() {
        int total = 0;
        for (Coin c : coins) {
            total += c.value();
        }
        return total;
    }
    public void lock() {
        assert !locked; // 鍵がかかっていないこと
        locked = true;
    }
    public void unlock() {
        assert locked;  // 鍵がかかっていること
        locked = false;
    }
}

※ 全貯金箱の合計金額を求めるためのgetAllAmount()メソッド, totalAllフィールドはここでは,削除しました.

コマンド入力方式を検討する

さて,最後の問題点(3)に対応するため,コマンドの入力方式について検討してみることにしましょう.先週のプログラムでは,コマンド(操作)とその対象となる貯金箱やコインの組合せに対して,あらかじめ番号を割り当てておき,その番号をユーザーに入力してもらう,というやり方を採用していました.この方法は,ユーザーの入力を解析しやすい,という利点はあるのですが,本稿のはじめの方で指摘した通り,コマンドの種類や対象となる貯金箱の数が増えると,あっという間に破綻します.この問題は,コインを入れる,鍵をかける,といったコマンドの種類と,その操作対象となる貯金箱の名前やコインの種類を特定の書式に従って,ユーザーが個別に指定できるようにすれば解決します.たとえば,「貯金箱Aに100円玉を入れる」という操作を実行するためには
"insert A 100"
というような文字列をユーザーが入力するようにします.その他の操作としては,鍵をかける(lock),鍵をはずす(unlock),貯金箱の一覧を表示する(list),といったものが考えられます.それぞれの操作の書式を以下のように整理します.

  • insert --- 貯金箱にコインを入れる
  • lock --- 貯金箱に鍵をかける
  • unlock --- 貯金箱の鍵をはずす
  • list --- 貯金箱の一覧を表示する
  • quit --- 終了する

※ コマンドと各操作対象の区切りは空白文字(スペースやタブなど)とする

全体の処理の流れは,以下のようになります.

while (true) {
    System.out.print("コマンドを入力してください:");
    // ユーザーからの入力を解析する
    // if (ユーザーの入力が終了命令) break;
    // 入力に応じた処理を実行する
}        

入力に応じた処理を実行するための,簡単な方法は,コマンドの種類ごとに分岐文を使う以下のようなやり方が思いつきます.

if (ユーザー入力が"insert") {
    // 貯金箱にコインを入れる処理
} else if (ユーザー入力が"lock") {
    // 貯金箱に鍵をかける処理
} else if...

このやり方でも良いのですが,ユーザーからの入力を解析する処理の部分で,入力文字列の種類ごとの分岐処理が必要あり,さらに実行の部分でも同じような分岐処理を入れるのは,あまり賢明なやり方ではありません.かといって,ユーザーからの入力を解析しながら,コマンド操作を実行するという方法も,プログラムがごちゃごちゃしてしまいそうです.

コマンドパターンによる各種操作の実装

そこで,今回はそれぞれの操作を対応するコマンドクラスのオブジェクトとしてカプセル化し,そのオブジェクトのメソッドを呼び出すことで処理を実行する,というやり方で実現していくことにします.
# デザインパターンにおけるコマンドパターンとして知られているやり方です

コマンドパターンを使って,全体の処理の流れを書き直すと以下のようになります.

while (true) {
    System.out.print("コマンドを入力してください:");
    // ユーザーからの入力を解析し,対応するコマンドオブジェクトを取得する
    Command command = parseCommand(...);
    // 終了操作の場合,nullが返される
    if (command == null) break;
    // 入力に応じた処理を実行する
    command.execute();
}        

ここでのポイントは,command変数に格納されるコマンドオブジェクトは,ユーザーが入力したコマンド文字列によって,「コインを挿入する」,「鍵をかける」など,異なる種類のものになる可能性がある,ということです.したがって,command.execute()で実際に実行される処理の内容もそのつど変わります.
# 専門的にはこのような性質を多態性(あるいは多相性,英語ではpolymorphism)といったりします.
オブジェクト指向プログラミングでは「継承」と呼ばれる機構を使うことによって,このような多態性を実現することができます.
# インタフェース(interface)を使ってもできますが,interfaceは制限された形の継承とみなすことができるので,結局は同じことです.

それでは,継承を使ってコマンドパターンを実装していくことにしましょう.まず,継承元のクラスであるCommandクラスを以下のように定義します.

abstract class Command {
    public abstract void execute();
}

たったこれだけです.この定義の特徴は,execute()メソッドの中身が空っぽで,メソッド名と引数,戻り値の型だけが指定されている点です.このように,中身の実装のないメソッドのことをJavaでは抽象メソッドと呼び,そのようなメソッドはabstract修飾子を使って明示的に示します.抽象メソッドを1つでも含んでいるようなクラスを抽象クラスと呼び,こちらもabstract修飾子を使って明示的に指定します.抽象クラスはインスタンスを作成することはできません.すなわち,以下のようなコードはコンパイラによってはじかれます.

Command c = new Command(); // コンパイルエラーになる

さて,このように中身のない空っぽのメソッドを定義して,何の意味があるのでしょうか?実は,抽象メソッドは,それを継承したクラスの中で具象メソッドとして実装される必要があります.すなわち,抽象クラスであるCommandクラスから継承したクラスを定義し,その中で改めてexecute()メソッドを定義する必要があるのです.このあたりは,章をあらためて詳しく説明することとし,ここではとりあえず先に進みましょう.

継承した貯金箱にコインを挿入するコマンドを表すCommandInsertクラスをCommandクラスから継承して以下のように定義します.

class CommandInsert extends Command {
    Coin coin;
    SavingsBox savingsBox;
    public CommandInsert(SavingsBox sb, Coin c) {
        this.savingsBox = sb;
        this.coin = c;
    }
    public void execute() {
        if (savingsBox.isLocked()) {
            System.out.println("貯金箱 '" + savingsBox.getName() + "'には鍵がかかっています.");
            return;
        }
        savingsBox.insertCoin(coin);
        System.out.println("貯金箱 '" + savingsBox.getName() + "'に" + coin.value() + "円を入れました."); 
    }
}

extendsキーワードの後には継承元となるクラス名を指定します.コンストラクタの引数として,対象となる貯金箱,コインの種類を指定します.引数で渡されたこれらのオブジェクトは,それぞれフィールドsavingsBoxとcoinに格納され,execute()メソッドでは,それらのオブジェクトが参照されます.execute()メソッドでは,まず貯金箱に鍵がかかっているかどうかをチェックし(これらのチェックは貯金箱クラスの利用者の責任としたことを思い出してください),鍵がかかっていないときだけ,コインを挿入して,その結果を標準出力に表示します.

同じように,鍵をかけるCommandLockや鍵をはずすCommandUnlock,貯金箱の一覧を表示するCommandListといったクラスも,それぞれ以下のように定義することができます.

// 鍵をかける
class CommandLock extends Command {
    SavingsBox savingsBox;
    public CommandLock(SavingsBox sb) { this.savingsBox = sb; }
    public void execute() {
        if (savingsBox.isLocked()) {
            System.out.println("貯金箱 '" + savingsBox.getName() + "'には鍵がかかっています.");
            return;
        }
        savingsBox.lock();
        System.out.println("貯金箱 '" + savingsBox.getName() + "'に鍵をかけました.");
    }
}
// 鍵をはずす
class CommandUnlock extends Command {
    SavingsBox savingsBox;
    public CommandUnlock(SavingsBox sb) { this.savingsBox = sb; }
    public void execute() {
        if (!savingsBox.isLocked()) {
            System.out.println("貯金箱 '" + savingsBox.getName() + "'に鍵はかかっていません.");
            return;
        }
        savingsBox.unlock();
        System.out.println("貯金箱 '" + savingsBox.getName() + "'の鍵をはずしました.");
    }
}
// 貯金箱の一覧を表示する
class CommandList extends Command {
    Collection<SavingsBox> savingsBoxes;
    public CommandList(Collection<SavingsBox> sbs) { this.savingsBoxes = sbs; }
    public void execute() {
        for (SavingsBox sb : savingsBoxes) {
            System.out.println("貯金箱 '" + sb.getName() + "' " + sb.getAmount() + "円" + (sb.isLocked() ? " (鍵がかかっています)" : "") );
        }
    }
}

これらのクラスはすべてCommandクラスから継承していますが,execute()の中身は異なっています.command.execute()で実際に呼び出されるexecute()は,例えば,command変数に格納されたオブジェクトがCommandListクラスのオブジェクトならば,その中のexecute()メソッドが呼ばれ,またCommandInsertクラスのオブジェクトならば,それのexecute()といった具合に,オブジェクトの種類によって,変わってきます.

1つ注意すべき点は,command変数にいろいろな種類のコマンドクラスのオブジェクトを代入できるのは,これらのクラスがすべてCommandクラスという1つのクラスから継承しているおかげだということです.Javaは静的に型付けされた言語なので,(BarがFooから継承したクラスでない限り)Fooというクラス型の変数にBarというクラス型のオブジェクトを代入することはできないのです.

コマンドパーサーの実装

さて,各コマンドクラスの実装は以上です.次に実際にユーザーからの入力文字列を解析して,これらコマンドクラスのオブジェクトを生成する処理を実装していくことにしましょう.

まずは,ユーザーから入力した文字列を空白文字を区切りとして,いくつかの固まり(トークンと呼びます)に切り分ける処理が必要です.たとえば,"insert A 100"という入力は,"insert", "A", "100"という3つのトークンに分解されます.そのためのクラスとして,JavaではStringTokenizerという便利なものがライブラリに用意されているので,今回はそれを使うことにします.

処理の流れは以下のようになります.

while (true) {
    String line = scanner.nextLine();
    StringTokenizer tok = new StringTokenizer(line);
    Command command = parseCommand(tok);
    if (command == null) {
        System.out.println("終了します");
        break;
    }
    command.execute();
}

parseCommandは,StringTokenizerクラス型の引数を取ります.parseCommandでは,引数で受け取ったStringTokenizerオブジェクトからトークンを1つ取り出し,その中身に応じたコマンドクラスのオブジェクトを生成し,returnします.

だいたい,以下のような感じになります.

static Command parseCommand(StringTokenizer tok) {
    String commandStr = tok.nextToken();
    if (commandStr.equals("insert")) {
        SavingsBox sb = parseSavingsBox(tok);
        Coin c = parseCoin(tok);
        return new CommandInsert(sb, c);
    } else if (commandStr.equals("lock")) {
        SavingsBox sb = parseSavingsBox(tok);
        return new CommandLock(sb);
    } else if
    ...
}

parseSavingsBox()は,トークンを取り出し,それに対応する貯金箱オブジェクトをテーブルから検索し,返します.また,parseCoin()も同様に対応するコインオブジェクトを返します.

さて,ここで考えなくてはならない重要なことに,エラー処理があります.ユーザーからの入力は間違った書式のコマンドが渡されてくる可能性があるので,そのような場合でも適切な対処をしなくてはなりません.たとえば,入力が空っぽだったり,スペルミスをしたり(inset A 100),存在しないコインが指定されたり(insert A 101)といったことが考えられます.そのような入力がなされたときは,(親切に)ユーザーにその旨をメッセージとして表示するようにし,改めて入力し直してもらうようにしましょう.

エラー処理の方針が決まったところで,次に考えるべきことは,エラーが発生したことを,どうやってメソッドの呼び出し元に知らせるか,ということです.伝統的なやり方では,メソッドの中でエラーが発生するような状況になったら,

if (エラー状態) {
   return エラーコード;
}

というような形でエラーコードを呼び出し元に返してやる,というものです.
呼び出し側では,

ret = obj.someMethod();
if (retがエラーコード) {
    // エラーに対処する
}

というような処理を書きます.

残念ながら今回のケースでは,メソッドの戻り値はコマンドオブジェクトとしているために,エラーコードを返す余地がありません.また,プログラムの大部分がエラーコードの判定に費やされてしまい,複雑になりがちです.そこで,例外を使うことにします.メソッドの中で不正な入力文字列を検出したらParseExceptionという例外を投げることにします.ParseExceptionはExceptionクラスから継承して以下のように定義します.

class ParseException extends Exception {
    public ParseException(String s) { super(s); }
}

最終的に,parseCommandの実装は以下のようになります.

private static Command parseCommand(StringTokenizer tok) throws ParseException {
    if (!tok.hasMoreTokens()) {
        throw new ParseException("コマンドが指定されていません");
    }
    String commandStr = tok.nextToken();
    if (commandStr.equals("insert")) {
        SavingsBox sb = parseSavingsBox(tok);
        Coin c = parseCoin(tok);
        return new CommandInsert(sb, c);
    } else if (commandStr.equals("lock")) {
        SavingsBox sb = parseSavingsBox(tok);
        return new CommandLock(sb);
    } else if (commandStr.equals("unlock")) {
        SavingsBox sb = parseSavingsBox(tok);
        return new CommandUnlock(sb);
    } else if (commandStr.equals("list")) {
        Collection<SavingsBox> sbs = savingsBoxTable.values();
        return new CommandList(sbs);
    } else if (commandStr.equals("quit")) {
        return null;
    }
    throw new ParseException("'" + commandStr + "' というコマンドはありません");
}

上記の中で,throw new ParseException(...);の部分が実際に例外を投げている部分です.Javaで例外を投げるにはthrowキーワードを用います.例外が発生した時のプログラムの実行はだいたい以下のようになります.
# throw文によって明示的に例外を投げる以外にも例外は発生します.たとえば,nullオブジェクトに対するメソッド呼び出しや,配列の範囲を超えたアクセスが生じた状況などです.

  • 例外が発生した箇所以降の文の実行は中止される.
  • 例外が発生する部分を取り囲む適切なtry〜catchがある場合,例外は捕捉されcatch節に実行が移る.
  • 例外が捕捉されない場合,メソッドの実行は中止され,そのメソッドの呼び出し元に同じ例外が発生する.

以上によって,捕捉されない例外はメソッドの呼び出し系列を次々にさかのぼって伝播していくことがわかります.
parseCommand()メソッドの場合,たとえば,読み出すべきトークンが空だとParseException例外を投げていますが,そこの部分はtry〜catchで囲まれていないので,その例外はparseCommandの呼び出し側に伝播します.parseCommandはこのParseException例外を発生させる可能性があるので,メソッド宣言のところで明示的にthrows ParseExceptionと記述する必要があります.この例外をry〜catchで捕捉する箇所は後ほど紹介します.

さて,同様にして,parseSavingsBoxとparseCoinも定義します.ここでは,貯金箱オブジェクトを格納した表savingsBoxTableとコイン一覧を格納した表coinTableがMainクラスの中で定義されていると仮定しています.

private static SavingsBox parseSavingsBox(StringTokenizer tok) throws ParseException {
    if (!tok.hasMoreTokens()) {
        throw new ParseException("貯金箱が指定されていません");
    }
    String sbStr = tok.nextToken();
    SavingsBox sb = savingsBoxTable.get(sbStr);
    if (sb == null) {
        throw new ParseException("'" + sbStr + "' という貯金箱はありません.");
    }
    return sb;
}
private static Coin parseCoin(StringTokenizer tok) throws ParseException {
    if (!tok.hasMoreTokens()) {
        throw new ParseException("コインが指定されていません");
    }
    String coinStr = tok.nextToken();
    Coin coin = coinTable.get(coinStr);
    if (coin == null) {
        throw new ParseException("'" + coinStr + "' というコインはありません.");
    }
    return coin;
}
これらのメソッドはparseCommandの中から呼び出されることに注意してください.parseSavingsBoxの中でParseException例外が投げられた場合,呼び出し元のparseCommandにその例外が伝播します.そしてその例外はさらに上位の呼び出し元まで伝播することになるのです.こうして,メソッド呼び出しの深い部分から一気に最上位の呼び出し元まで処理を戻すことができるようになります.

最後に,mainメソッドを含むMainクラスの定義を以下に示します.

class Main {
    private static HashMap<String, SavingsBox> savingsBoxTable;
    private static HashMap<String, Coin> coinTable;

    public static void main(String[] args) {
        // 貯金箱テーブルの初期化
        savingsBoxTable = new HashMap<String, SavingsBox>();
        savingsBoxTable.put("Pig", new SavingsBox("Pig"));
        savingsBoxTable.put("Cat", new SavingsBox("Cat"));
        savingsBoxTable.put("Dog", new SavingsBox("Dog"));
        // コインテーブルの初期化
        coinTable = new HashMap<String, Coin>();
        coinTable.put("1", Coin.Y1);
        coinTable.put("5", Coin.Y5);
        coinTable.put("10", Coin.Y10);
        coinTable.put("50", Coin.Y50);
        coinTable.put("100", Coin.Y100);
        coinTable.put("500", Coin.Y500);

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("コマンドを入力してください:");
            String line = scanner.nextLine();
            StringTokenizer tok = new StringTokenizer(line);
            try {
                Command command = parseCommand(tok);
                if (command == null) {
                    System.out.println("終了します");
                    break;
                }
                command.execute();
            } catch (ParseException e) { // ParseException発生時の処理
                System.out.println(e.getMessage());
                continue;
            }
        }
    }
    // parseCommandの定義
    ...
    // parseSavingsBoxの定義
    ...
    // parseCoinの定義
    ...
}

parseCommand呼び出しの部分を,try〜catch(ParseException e)というように囲むことによって,ParseException例外を捕捉するようにしています.parseCommandを呼び出した際にParseException例外が発生したら,catch節にただちに実行が移ります.そして,エラーメッセージを表示して,continue文によってふたたびwhileループの最初から処理をやり直すようにしています.

実行例は以下のようになります.

コマンドを入力してください:list
貯金箱 'A' 0円
貯金箱 'B' 0円
貯金箱 'C' 0円
コマンドを入力してください:insert B 500
貯金箱 'B'に500円を入れました.
コマンドを入力してください:insert A 123
'123' というコインはありません.
コマンドを入力してください:lock A
貯金箱 'A'に鍵をかけました.
コマンドを入力してください:list
貯金箱 'A' 0円 (鍵がかかっています)
貯金箱 'B' 500円
貯金箱 'C' 0円
コマンドを入力してください:unlock B
貯金箱 'B'に鍵はかかっていません.
コマンドを入力してください:quit
終了します

まとめ

今回は,リファクタリングをテーマにしつつ,それに役立ついろいろな新しい概念を駆け足で紹介してきました.たとえば,列挙型,継承,抽象クラス,多態性,例外などなどです.これらの詳しい説明はまた章を改めて紹介したいと思いますので,今日は雰囲気だけ掴んでいただければと思います.