繁殖シミュレーション/空間クラス

空間クラスの設計

次に、空間をどのように実現するか、を考えましょう。要件から、図20のように空間はセルが並べられた1次元の列で左端と右端が繋がっている必要があります。

1次元の空間のイメージ

図20:1次元の空間のイメージ

セルの列は、配列やリスト構造を使えば実現できます。

セルの列では、列の長さを動的に変更する必要が特に無いので、配列が良さそうです。 配列には、先に考えた生物オブジェクトをそのまま格納すればよいでしょう。

前述した、Emptyオブジェクトが空の状態を表しますから、配列の各要素は基本的にEmptyオブジェクトで初期化されているとすれば素直な割り当て方となります。

左端と右端が繋がっている構造は、例えば配列aであれば、図21のように、a[-1]のときa[a.length-1](逆にa[a.length]のときならばa[0])となるように、配列の長さによる剰余を求めることで、インデックス値の修正を参照するたびにしてあげれば良いでしょう。この修正インデックスは図22に示す方法で計算できます。

周期的な1次元空間の実現

図21:周期的な1次元空間の実現
if (インデックスが正の値であれば) {
    
    return インデックス値 % 空間サイズ;
    
} else {
    // インデックス値が負のとき、負の剰余は例えば-4 % 3 = -1となります
    return (空間サイズ + インデックス値 % 空間サイズ) % 空間サイズ;
}
図22:周期的なインデックス値の計算方法

よって、これら機能を満たす空間クラスを作れば良さそうです。ここで、1つの空間オブジェクトが1ヶ月ごとの空間を表現するとすれば、より自然な定義になります。


これまで考察してきた空間について必要最低限の振る舞いを抽出すると、以下になります。

  • インデックス値を周期的インデックス値に補正する。
  • 空間に生物を配置する。
  • 空間から生物を取得する。
  • 1ヶ月後の空間を計算する。
  • 空間の状態を表示する。

ここで、空間の概念が抽象的かどうかについて議論する必要があります。

明らかに空間は抽象的な概念ですが、今回のようにたとえば2次元の空間に拡張する予定がない場合、無理に抽象化する必要はありません。

\({\Huge``}\) 柔軟で複雑な設計というものは、ほとんどの場合、 必要とされないことが多いのです 。

\(\hspace{1pc}\) As for the flexible, complex design, most of the time you aren't going to need it .\({\Huge,\hspace{-0.4em},}\)

------ Martin Fowler, リファクタリング: プログラミングの体質改善テクニック. p.68[15]

抽象化を推し進めた結果、出来上がった柔軟すぎるプログラムは過度に複雑化し、保守が難しくなります。過度な機能(ここでは抽象化)は必要とされないことが多いのです。必要な機能と判断されるまでなるべく機能を持たせない(抽象化しない)、なるべくシンプルに設計するというスローガンはYou Aren't Going to Need Itの頭文字から取って YAGNI と呼ばれます。

以上の検討から、ひとまず空間の振る舞いは全て具象メソッドとし、空間クラスOneDimensionalSpaceを図23のように定義します。

空間クラスと生物クラスの関係

図23:空間クラスと生物クラスの関係

クラス図のフィールドは実装として書き下す時、図24のルールに則って記述します。

アクセシビリティ記号 フィールド名: フィールドの型
図24:クラス図でのフィールドの書き方

なお、図23のクラス図中にある実線矢印は 関連(Association) を表します。関連とは、クラス間にクラスやオブジェクトの参照関係がある場合、結ぶことができます。矢印の方向は、その方向にのみ関連がある場合に引かれます。このような関係を knows-a 関係と呼びます。

線の付け根に割り当てられている数字は、オブジェクト間の関連の数(いくつ参照・保持しているか)で、多重度(Cardinarity) を表します。具体的な数値以外にも、0..*(0以上)、1..5(1以上5以下)のように任意の範囲を指定できます。図22においては、OneDimensionalSpaceオブジェクト1つにつき、1以上の Life オブジェクトが関連していることを表します。

冒頭で空間を表すセルの列を生物クラスの配列として表すことに決めたと思います。つまり、OneDimensionalSpaceクラスは、 Life オブジェクトを配列でまとめて持つわけです。

このように、独立して存在できるオブジェクトをリストや配列などでまとめて保持することを 集約(Aggregation) と呼びます。また、このような関係を has-a 関係と呼びます。

OneDimensionalSpaceクラスと Life クラスの関係をより的確に示すのであれば、knows-a関係と言うよりはhas-a関係となるはずです。したがって、図25のように集約としてクラス図で表したほうが良いです。

空間クラスと生物クラスの集約

図25:空間クラスと生物クラスの集約

クラス図において集約は白抜きダイヤモンド矢印で表されます。例えば、図25では、OneDimensionalSpaceオブジェクトは Life オブジェクトを集約している(Life オブジェクトはOneDimensionalSpaceオブジェクトの一部である)、ことを表しています。

ここで図25で示しているように、空間の必要最低限の振る舞いを以下のメソッドに割り当てています。

  • インデックス値を周期的インデックス値に補正する。
    • calculateIndexメソッド
  • 空間に生物を配置する。
    • setLifeメソッド
  • 空間から生物を取得する。
    • getLifeメソッド
  • 1ヶ月後の空間を計算する。
    • nextメソッド
  • 空間の状態を表示する。
    • printメソッド

1ヶ月後の空間の計算

ここまで、空間クラスの振る舞いを具体的に抽出して、OneDimensionalSpaceクラスとしてメソッドのシグネチャやデータ構造までは設計できました。次に、OneDimensionalSpaceオブジェクトが1ヶ月後の空間をnextメソッドでどのように計算すればよいか考えてみましょう。

まず、1ヶ月後の空間を作るためには、各セルの状態を全て同時に計算しなければなりません。なぜならば、各セルに居るであろう生物は、自分の1ヶ月後の状態を、現在の自分の状態および両隣のセル(にいる生物)の状態を元に決定します。

仮に、順に生物の状態を計算していくと、図26に示すように、1ヶ月後を計算済みの生物を元に、1ヶ月後を計算してしまう生物が現れてしまいます。これでは、正確な1ヶ月後の空間の計算ができません。

空間のセルを順に更新していく場合

図26:空間のセルを順に更新していく場合

では、どのように各セルの状態を全て同時に計算させるべきでしょうか。OneDimensionalSpaceクラスのspaceフィールドに対して計算結果を反映させながら同時に計算することは、先の問題によってできません。

しかし、1ヶ月後の状態を保持する新たな空間としてOneDimensionalSpaceオブジェクトを別に作成しておき、その空間オブジェクトに対して、図27のように現在の状態を元に計算した1ヶ月後の状態を順に割り当てていくことはできます。

1ヶ月後の空間オブジェクトに対して各セルの更新結果を反映

図27:1ヶ月後の空間オブジェクトに対して各セルの更新結果を反映する場合

全生物オブジェクトの1ヶ月後の状態を別途用意した空間オブジェクトに割り当てることができれば、図10におけるフローチャート中の次のオブジェクトの状態を計算したことになり、1ヶ月後の空間オブジェクトを現在の空間オブジェクトに置き換えることでオブジェクトに計算結果を反映したことになります。


次に、各生物の1ヶ月後の状態をどのように決定するか、を考えなければなりません。冒頭の繁殖シミュレーションの要件のうち、次の3項目が状態決定に関係します。

  • 生物は設定された寿命になると死滅する。
  • 生物が死滅した場合、空間から取り除かれる。
  • 生物は1ヶ月ごとに隣接するセルに対して繁殖を行う。

草クラスの状態更新

現時点では草クラスGrassと空の状態クラスEmptyのみ、具体的な生物クラスとして図19と図18のように実装しています。まずGrassオブジェクトを元に上記要件をどのように満たしていくべきか考えましょう。

生物は設定された寿命になると死滅する。

前述した草であるGrassクラスの振る舞いを検討した際に、growメソッドを実装しました。growメソッドは、成長を表していて、年齢(月齢)をインクリメントします(1ヶ月だけ年齢を加算します)。growメソッドによって事前に設定されている寿命を年齢が超えると、死んでいるかどうか判定するisDeadメソッドの結果がtrueを返すようになります。

したがって、オブジェクトごとの状態計算をするたびに1回growメソッドを呼び出して1ヶ月成長させればよいです。

生物が死滅した場合、空間から取り除かれる。

次に、isDeadメソッドを使って、成長させたオブジェクトが死んでいるかどうか判定します。isDeadメソッドの結果に応じて、次状態の空間に対して成長させた生物オブジェクトを割り当てるか割り当てないかを判断すれば良いです。

オブジェクトが死んでいれば、空の状態を表すEmptyオブジェクトを新たに作成し、次状態の空間として別に用意したOneDimensionalSpaceオブジェクトの同じ位置に割り当てます。逆に死んでいなければ、成長させたオブジェクトを代わりに割り当てればよいです。

生物は1ヶ月ごとに隣接するセルに対して繁殖を行う。

Grassクラスでは、同様にreproduceメソッドを実装しました。繁殖成功率に基づいて、新たなGrassオブジェクトか、Emptyオブジェクトを返却します。

よって、オブジェクトが生きている時に、現在の空間と次状態の空間において隣接するセルが空であれば、次状態の空間オブジェクトの対応する隣接セルにreproduceメソッドの結果を割り当てればよいです。

ここで、セルが空であることをどのように判断するかというと、Emptyオブジェクトが割り当てられていれば空、それ以外のオブジェクトが割り当てられていれば非空であるとわかります。

Javaでは、クラスがある上位クラスを継承または実装しているかどうか判定する演算子instanceofがあります。例えば、a instanceof Bは、変数aに割り当てられているオブジェクトがクラスBを継承または実装しているのならば、trueを返却し、そうでなければ、falseを返却します。図28に利用例を示します。

instanceof演算子を活用することで、あるオブジェクトがEmptyクラスかどうか判断できます。

public void check(Life a) {

	if (a instanceof Empty) {

		System.out.println("オブジェクトは空の状態です。");
	}
}
図28:instanceof演算子の利用例

空(から)状態のクラスの状態更新

次に、図18のEmptyオブジェクトが上記要件をどのように満たしていくべきか考えなければなりません。

生物は設定された寿命になると死滅する。

空の状態は不変です。死滅することはありません。Emptyクラスで実装したgrowメソッドは何もしません。

したがって、Grassオブジェクトと同様に、状態計算のたびに実装させれているgrowメソッドを呼び出したとしても問題ありません。

生物が死滅した場合、空間から取り除かれる。

Emptyクラスの検討において、空の状態は死んでいる状態と仮定しました。つまり、isDeadメソッドは常にtrueを返却します。

したがって、Grassオブジェクトの時と同じで死んだ状態のセルには、空の状態を表すEmptyオブジェクトを新たに作成して、次状態の空間オブジェクトに割り当てるだけで良いです。

ただ、他の具体的な生物オブジェクトが隣接していた場合、そのオブジェクトの繁殖によって次状態の空間にすでに新個体が割り当てられている可能性があります。よって、次状態の空間が空の状態であることを確認した上で、空であれば自身を割り当て直す、空でなければなにもしない、とすればよいです。

生物は1ヶ月ごとに隣接するセルに対して繁殖を行う。

空の状態はダミー生物のようなものです。繁殖できません。Emptyクラスで実装したreproduceメソッドは、自身を返却します。

Emptyオブジェクトは常に死んだ状態です。つまり、Grassオブジェクトのように生きている状態に繁殖するといった、生きている状態の処理を考慮する必要がありません。ですから、特に何もしなくて良いです。


上記で考察した2つの生物クラスにおける振る舞いを統合すると、図29のフローチャートになります。

生物オブジェクトの1ヶ月後の状態計算のフローチャート

図29:生物オブジェクトの1ヶ月後の状態計算のフローチャート

空間クラスの実装

それでは、これまでの検討から、空間クラスOneDimensionalSpaceの振る舞いを以下にまとめます。

  • calculateIndexメソッド(インデックス値を周期的インデックス値に補正する)
    • 図22のアルゴリズムに則って、与えられたインデックス値を補正し、その値を返却する。
  • setLifeメソッド(空間に生物を配置する)
    • フィールドの配列spaceに対して、引数として与えられたインデックス値が指し示す位置に、引数として割り当てられた生物オブジェクトを割り当てる。
      • 引数で与えられたインデックス値は、calculateIndexメソッドによって補正後返却された値を利用する。
  • getLifeメソッド(空間から生物を取得する)
    • フィールドの配列spaceにおいて、引数として与えられたインデックス値が指し示す位置にある生物オブジェクトを返却する。
      • 引数で与えられたインデックス値は、calculateIndexメソッドによって補正後返却された値を利用する。
  • nextメソッド(1ヶ月後の空間を計算する)
    • 新しい空間オブジェクトを作成して1ヶ月後の空間オブジェクトとし、図28のフローに則って、その空間オブジェクトを更新し、更新した空間オブジェクトを返却する。
      • フィールドの配列spaceに割り当てられている全ての生物オブジェクトを、getLifeメソッドを使ってインデックスを先頭から順に変更しながら取得し、1ヶ月後の空間オブジェクトの同一インデックス値の位置に、更新した生物オブジェクトをsetLifeメソッドを使って割り当てる。
  • printメソッド(空間の状態を表示する)
    • フィールドの配列spaceの要素を1つづつ取り出し、全て標準出力する。

また、補足すべき振る舞いとして、以下が挙げられます。

  • コンストラクタで、空間のセル列の長さを設定する。
    • 引数で与えられた長さのLifeクラスの配列を作り、全てのインデックスをEmptyオブジェクトで初期化する。

以上を踏まえると、空間クラスは、図30のように実装できます。

/**
 * 生物が存在する1次元空間です。
 * @author Me
 */
public class OneDimensionalSpace {

    /**
     * 空間構造の元となる配列です。
     */
    private Life[] space;
    
    /**
     * 1次元空間を作ります。
     * @param size 整数で表される1次元空間の大きさ。
     */
    public OneDimensionalSpace(int size) {
        
        this.space = new Life[size];
        
        // 空間をEmptyオブジェクトで初期化する。
        for (int i = 0; i < size; i = i + 1) {
            
            this.space[i] = new Empty(i);
        }
    }
    
    /**
     * 環状の1次元空間を表すためのインデックスを計算します。
     * @param x 補正前のインデックス。
     * @return 環状になるように補正したインデックス。
     */
    private int calculateIndex(int x) {
        
        if (x >= 0) {
            
            return x % this.space.length;
            
        } else {
            
            return (this.space.length + x % this.space.length) % this.space.length;
        }
    }
    
    /**
     * 生物を空間に配置します。
     * @param life 配置する生物。
     */
    public void setLife(Life life) {
        
        int index = life.getIndex();
        
        // 指定されたインデックスを環状に補正します。
        int fixi = this.calculateIndex(index);
        
        this.space[fixi] = life;
    }
    
    /**
     * 指定された空間インデックスに配置された生物を取得します。
     * @param i 空間インデックス。
     * @return 空間インデックスに割り当てられている生物オブジェクト。
     */
    public Life getLife(int i) {
        
        return this.space[this.calculateIndex(i)];
    }
    
    /**
     * 1ステップ後の空間を計算します。1ステップは1ヶ月として換算します。
     * @return 求められた1ステップ後の空間オブジェクト。
     */
    public OneDimensionalSpace next() {
        
        // 新たな空間オブジェクトを作成します。
        OneDimensionalSpace nextSpace = new OneDimensionalSpace(this.space.length);
        
        // 現在の空間にいる生物の状態から次の状態を決定するために、全ての空間インデックスについて見ていきます。
        for(int i = 0; i < this.space.length; i = i + 1) {

            // 生物オブジェクトを取得します。
            Life center = this.getLife(i);
            // 1ヶ月生物オブジェクトを成長させます。
            center.grow();
            
            int leftIndex = i - 1;
            int rightIndex = i + 1;

            Life left = this.getLife(leftIndex);
            Life right = this.getLife(rightIndex);

            // 次の空間オブジェクトのこの植物オブジェクトが居るインデックスが空のとき
            if (nextSpace.getLife(i) instanceof Empty) {
                // 生殖フェーズ
                if (center.isDead()) {
                    // 生物オブジェクトが死んでいた場合。

                    // 次の空間では空(から)の状態にします。
                    nextSpace.setLife(new Empty(i));

                } else {
                    // 生物オブジェクトが生きていた場合。

                    nextSpace.setLife(center);

                    Life leftNext = nextSpace.getLife(leftIndex);
                    Life rightNext = nextSpace.getLife(rightIndex);

                    if (left instanceof Empty && leftNext instanceof Empty) {
                        // 現在の空間インデックスの左側の生物オブジェクトを繁殖させます。
                        Life leftNew = center.reproduce();
                        
                        // 生物オブジェクトに左側のインデックスを設定します。
                        leftNew.setIndex(leftIndex);
                        
                        // 空間に生物オブジェクトを配置します。
                        nextSpace.setLife(leftNew);
                    }

                    if (right instanceof Empty && rightNext instanceof Empty) {
                        // 現在の空間インデックスの右側の生物オブジェクトを繁殖させます。
                        Life rightNew = center.reproduce();
                        
                        // 生物オブジェクトに右側のインデックスを設定します。
                        rightNew.setIndex(rightIndex);

                        // 空間に生物オブジェクトを配置します。
                        nextSpace.setLife(rightNew);
                    }
                }
            }
        }
        return nextSpace;
    }

    /**
     * 空間の中身を表示します。
     */
    public void print() {
        
        for (int i = 0; i < this.space.length; i = i + 1) {
            
            System.out.print(this.space[i].toString());
        }
        System.out.println();
    }
}
図30:空間を表すOneDimensionalSpaceクラスの例

シミュレーションの実装

図10のシミュレーションのフローを元に、OneDimensionalSpaceクラスと具体的な種クラスGrassを使って、シミュレーションを実装してみましょう。 図31に実装したmainメソッドの例を示します。

この例では、セル列の長さが50である空間を仮定し、インデックス0に草オブジェクトを初期配置して、4年間分48ヶ月の各月ごとの空間の様子を表示させます。

public static void main(String[] args) {
    
    // 初期空間を生成します。
    int size = 50;
    
    OneDimensionalSpace space = new OneDimensionalSpace(size);

    // 草をインデックス0に配置します。
    space.setLife(new Grass(0));
    // 初期空間を表示します。
    space.print();
    
    // 12ヶ月 * 4 = 4年間
    int iteratedMonth = 12*4;
    
    // シミュレーションを行います。
    for (int i = 0; i < iteratedMonth; i = i + 1) {
        
        // 次の世代の空間を計算します。
        space = space.next();
        // 作成した世代の空間を表示します。
        space.print();
    }
}
図31:mainメソッドの例

[15]: M. Fowler, ``リファクタリング: プログラミングの体質改善テクニック'', ピアソン・エデュケーション社 (2000)