Go to Previous Page Go to Contents Go to Java Page Go to Next Page
Second Step of Java
 
 

ハノイの塔を組み立てなおす

 
 

もう一度クラス設計

 
 

前回までで、とりあえずハノイの塔が遊べるところまで作ってきました。でも、なんか気に入らないのです。なんだろうと、自分自身で考えてみると、結局

  • ハノイの塔をあらわすオブジェクトが List オブジェクトになっているのでぱっと見るとよく分からない

ということに尽きるような気がします。たしかにハノイの塔をあらわすオブジェクトの名前は towers にしましたが、やはり List オブジェクトであることには変わらないのです。例えば、ぜんぜん関係のない人がこれを見たときに List オブジェクトが 3 つの Stack オブジェクトから成っており、それぞれが塔に相当しているとわかるでしょうか。難しいと筆者は思うのです。

第 3 者でなくとも、例えば 1 年後にこのプログラムを見たときに自分で分かるだろうかと考えてしまうわけです。

やっぱり、分かりやすいプログラムにするほうがいいですよね。

結局、行きつくところは

  • ハノイの塔をあらわすクラスが必要ではないか

ということです。

もう一箇所、気になる部分があります。それは Give up をしたときに例外を発生させるという方法です。例外というのはあくまでも、アプリケーションが異常な状態にあることを知らせるために使うもので、制御の流れに使うものでないと思います。例外に頼らずに、Give up を伝えるようにしたいのです。

今回は、これらの点を考慮してもう一度クラスを見直していこうと思います。

 

 
  ハノイの塔をあらわすには  
 

今まではハノイの塔をあらわすときには List オブジェクトを使用し、List オブジェクトの要素として塔に相当する Stack オブジェクトを使用しました。Stack には円盤の大きさを保持させています。

ハノイの塔をクラスであらわすときには、今までの構造を参考にして行うことができます。例えば、次のような方法が考えられます。

  1. ハノイの塔のクラスの属性として List オブジェクトを持たせる。List オブジェクトの保持する情報は今までのものと同じ
  2. ハノイの塔のクラスの属性として List オブジェクトを持たせる。List オブジェクトには塔に相当するオブジェクトを保持させる。塔のクラスは Stack オブジェクトを属性として持ち、円盤の大きさを Stack オブジェクトに入れる
  3. ハノイの塔のクラスの属性として List オブジェクトを持たせる。List オブジェクトには塔に相当するオブジェクトを保持させる。塔のクラスは Stack オブジェクトを属性として持ち、Stack オブジェクトには円盤に相当するオブジェクトを保持させる

この 3 種類の方法はどこまでをクラスとしてあらわすかということが異なってきます。1 はハノイの塔をあらわすクラスだけ。2 はハノイの塔と 1 本の塔をあらわすクラス。3 はハノイの塔、1 本の塔、円盤の 3 種類のクラスを使用する方法です。

今回は練習だと思って、ハノイの塔、塔、円盤の全部をクラスであらわしてみましょう。ハノイの塔は HanoiTowers クラス、塔は Tower クラス、円盤は Pile クラスとしました。この 3 種類のクラス含めてクラス図を書き直してみます。まだ、関数などは決まっていないので、クラス間の関連だけを図 3-18 に示しました。

更新したクラス図

図 3-18 更新したクラス図

 

ハノイの塔は常に 3 本の塔があるので、HanoiTowers は 3 つの Tower オブジェクトを持つようになります。それがHanoiTowers クラスから Tower クラスへの関連の横に書いてある数字の意味です。Pile は Tower にささっていることもあるし、ささっていないこともあるので 0 以上をあらわすアスタリスクが書いてあります。

また、HanoiTowers は常に 3 本の塔を持っているので、HanoiTowers オブジェクトが生成されたときには、3 つの Tower オブジェクトを HanoiTowers オブジェクトが生成します。同様に HanoiTower オブジェクトが消滅するときは、3 つの Tower オブジェクトを消滅させます。この関係は図 3-14 であらわしたコンポジッションになります。HanoiTowers クラスから Tower クラスへの関連が塗りつぶされた菱形なのはこのためです。

Tower クラスと Pile クラスは、Tower クラスが Pile クラスを管理しますが、Tower オブジェクトと Pile オブジェクトのライフサイクルは異なるので、関連は集約になります。

このようなくラス構成にしたとき、HanoiTowers などのクラスはどのような機能を持たせればいいでしょうか。

自分のことは自分で行うという指針で行きましょう。例えば、円盤の移動は Hanoi オブジェクトが行うのではなく、HanoiTowers 自身が行うようにすることです。このように考えると、Hanoi クラスと Player クラスの機能で HanoiTowers クラスに移せそうなものは

  • 円盤の移動
  • 塔の表示
  • 円盤が移動できるかどうかのチェック
  • 終了チェック

があげられます。

塔の表示には円盤の表示や塔の表示を組み合わせて行ったことを思い出してください。これはそのまま、Tower クラスや Pile クラスの機能になるはずです。

そのほかの機能を考えてみましょう。円盤の属性としては大きさが重要になります。これは今まで、Stack オブジェクトに円盤の大きさだけを入れていたことを考えれば合点がいきますね。この属性を取り出すための getter 関数が必要になります。これをまとめると Pile クラスは次のようになります。

Pile クラス

ハノイの塔の円盤をあらわすクラス。

属性 private int size 円盤の大きさ
private int maxSize 円盤の最大の大きさ
メンバ関数 public int getSize()

円盤の大きさを返す

public void print() 円盤を表示する関数

 

Tower クラスはどうなるでしょう。今まで、Tower クラスは Stack クラスで代用していたわけですから、Stack クラスで行っていた push, pop, peek を行う必要があります。そのほかには、Hanoi クラスの中で Stack クラスに対する操作としては、円盤が移動できるかどうかのチェックを行うときに Stack のサイズを調べる、Stack が空かどうかを調べるという 2 つの操作を行っていました。また、塔を表示するときにクーロンの生成を行っています。

今まで Stack クラスで行っていたこれらの機能を実装するためにはどうすればいいでしょうか。簡単なのは Tower クラスの属性として Stack オブジェクトを持たせてしまうことです。例えば Tower クラスの peek 関数がコールされた場合、属性として持っている Stack オブジェクトの peek 関数をコールするようにしてしまえばいいのです。

これらをまとめると

Tower クラス

塔をあらわすクラス。

属性 private Stack piles 円盤を保持するためのスタック
private int maxSize 塔にささる最大の円盤数
メンバ関数 public void pushPile(Pile pile)

円盤を塔にさす

public Pile popPile() 一番上の円盤を取り出す
public Pile peekPile() 一番上の円盤のコピーを取り出す
public int getNumber() 塔にささっている円盤の枚数を返す
public boolean isEmpty() 塔に円盤がささっているかどうかを返す
public Object clone() 塔の複製を作成する
public void print() 塔の表示を行う

 

最後に残ったのが HonoiTowers クラスです。すでに示したように Hanoi クラスと Player クラスの機能で HanoiTowers クラスに移せそうなものは

  • 円盤の移動
  • 塔の表示
  • 円盤が移動できるかどうかのチェック
  • 終了チェック

があります。表示には print 関数以外にも private な関数群がありましたので、これも忘れないように HanoiTowers クラスに打ちしましょう。属性として、3 つの Tower オブジェクトがあります。後は円盤の最大サイズを持つようにしましょう。

HanoiTowers クラス

ハノイの塔をあらわすクラス。

属性 private static final int NUMBER_OF_TOWERS 塔の本数 (3本)
private int maxSize 塔にささる最大の円盤数
private List towers Tower オブジェクトを保持するためのリスト
メンバ関数 public void move(int source, int destination)

円盤の移動

public boolean checkCompletion(int goal) 終了チェックを行う
public boolean isMovable(int source, int destination) 円盤の移動チェック
private Tower getTower(int i) i 番目の Tower オブジェクトを返す
public void print() ハノイの塔を表示する
private void printTowerNumber(int num) 塔の番号を表示する
private void printBase() 塔の台座を表示する

 

このように新たにハノイの塔をあらわすクラスを作ったことで、今までの Hanoi クラスと Player クラスの属性、関数が少なくなります。属性や関数まで記述したクラス図を図 3-19 に示しておきますので、今までのクラス構成と見比べてみてください。

更新したクラス図

図 3-19最終的なクラス図


図 3-19 には見慣れない記号があります。丸から線が延びていて、それが Tower クラスと Pile クラスにつながっています。丸の上には Cloneable となっています。Cloneable は前節で説明したように、オブジェクトの複製が作れるかどうかを示すインタフェースでしたね。

じゃんけんの時 (図 2-7) は、インタフェースをインプリメントするとき三角と点線を用いていました。インタフェースをきちんとあらわすのであればこれでいいのですが、簡易的にインタフェースをあらわすときに図 3-19 のように丸に線で表します。この丸に線をロリポップといいます。

図 3-19 では Tower クラスと Pile クラスが Cloneable インタフェースをインプリメントしていることを示しています。

 

 
  ハノイの塔を再び組み立てる  
 

再設計をしたものを実装していきましょう。とはいうものの、実際には Hanoi クラスや Player クラスで実装したものを HanoiTowers クラスなどに移しているだけなのでそれほど手間はかかりませんでした。

完成版を次に示しておきます。

Hanoi クラス Hanoi.java
Player クラス Player.java
HanoiTowers クラス HanoiTowers.java
Tower クラス Tower.java
Pile クラス Pile.java

例えば、Player クラスの nextStep 関数は次のように変わっています。変更した部分を赤で示してあります。

    public boolean nextStep(HanoiTowers towers){
        while(true){
            String inputText = null;
            try{
                System.out.print("どこのリングを動かしますか? (g で Give Up) [0-2] ");
                inputText = reader.readLine();
 
                if(inputText.equalsIgnoreCase("g")){
                    return false;
                }
 
                int x = Integer.parseInt(inputText);
 
                System.out.print("どこへ動かしますか? [0-2] ");
                inputText = reader.readLine();
                int y = Integer.parseInt(inputText);
 
                if(towers.isMovable(x, y)){
                    towers.move(x, y);
                }else{
                    System.out.println("動かせません。もう一度入力してください。");
                    continue;
                }
 
                return true;
            }catch(IOException ex){
                ex.printStackTrace();
                System.exit(1);
            }catch(NumberFormatException ex){
                System.out.println("入力が間違っています。もう一度入力してください。");
                continue;
            }
        }
    }

今までは nextStep 関数を抜けてから Hanoi オブジェクトが円盤の移動を行っていたのですが、移動のチェックができたらすぐに移動を行うようにしてしまいました。このため、PileMovementInfo クラスを戻り値にしなくてよくなりました。

ということは、Give up の情報を戻り値として使用することが可能になります。そこで、nextStep 関数の戻り値は boolean にし、戻り値が false であれば、Give up をあらわしていることにしました。

その他に、変更した部分といえば Hanoi クラスの makeTowers 関数がなくなってしまいました。これは HanoiTowers クラスのコンストラクタになっています。

    private List makeTowers(){
        List towers = new ArrayList();
 
        for(int i = 0 ; i < NUMBER_OF_TOWERS ; i++){
            towers.add(new Stack());
        }
 
        for(int i = size ; i > 0 ; i--){
            ((Stack)towers.get(START)).push(new Integer(i));
        }
 
        return towers;
    }

これが、次のようになりました。

    public HanoiTowers(int maxSize, int initialPosition){
        this.maxSize = maxSize;
        towers = new ArrayList();
        
        for(int i = 0 ; i < NUMBER_OF_TOWERS ; i++){
            if(i == initialPosition){
                towers.add(new Tower(maxSize, true));
            }else{
                towers.add(new Tower(maxSize, false));
            }
        }
    }

行っていることは基本的に同じなのですが、Stack オブジェクトに内容を入れる処理は Tower クラスが肩代わりするのでなくなっています。その代わりに、円盤を積むかどうかを示すフラグを Tower クラスのコンストラクタの引数に入れました。

新たに記述した部分では Tower クラスの popPile, peekPile, pushPile 関数です。これらの関数は属性で持っている Stack オブジェクトの pop, peek, push 関数を呼びだしています。これらの関数の戻り値は Object クラスですが、Pile オブジェクト以外を Stack オブジェクトに格納することはないので、Pile クラスにキャストしてあります。

    private Stack piles;
 
    public Pile popPile(){
        return (Pile)piles.pop();
    }
 
    public Pile peekPile(){
        return (Pile)piles.peek();
    }
 
    public void pushPile(Pile pile){
        piles.push(pile);
    }

ここで示したように、ソフトウェアの設計と実装は 1 回だけで済むものではありません。何度か設計と実装をくり返し行うことでよりよいソフトウェアを完成させるようにしましょう。

 

 
 

作成したソースファイルとコンパイルを行ったクラスファイルはここでダウンロードできます hanoi3.zip

(Feb. 2001)

 
 
Go to Previous Page Go to Contents Go to Java Page Go to Next Page