繁殖シミュレーション/生物クラス

この節の要点

  • プログラムを構成するクラスをどの様に作ればよいか、確認するための図式記法としてクラス図がある。
  • final修飾子をつけた変数は、変更できないデータを表し、特にstatic final修飾子は定数を定義する際に利用される。

ここからは、抽象クラスをどのように利用していくか見ていくために、図9のような生物の繁殖シミュレーションプログラムを作成していきましょう。

繁殖シミュレーションとは

繁殖シミュレーションのイメージ

図9:繁殖シミュレーションのイメージ

シミュレーションプログラムは基本的に図10に示す処理の流れに則って形作られます。

シミュレーションプログラムのフロー例

図10:シミュレーションプログラムのフロー例

図10のように、シミュレーションとは、先にオブジェクトごとに定められたルールに則って次状態を全て計算し、その後全オブジェクトを次状態へと更新する、といった繰り返し処理を規定回数行い、その結果を示すこと指します。 つまり、繰り返し処理をいかに後から改変可能なプログラム構造にするか、がシミュレーションプログラムにおいては仕様変更に強いプログラムを作るコツになるわけです。

ここで、今回作成するシミュレーションを行う上での仮定、すなわちシミュレーションプログラムの要件を示しておきましょう。

  • セルが並べられた1次元の空間に生物を配置する。
  • 1次元の空間は左端と右端が繋がっている環状構造とする。
  • 生物は設定された寿命になると死滅する。
  • 生物が死滅した場合、空間から取り除かれる。
  • 生物は1ヶ月ごとに隣接するセルに対して繁殖を行う。
  • 1ヶ月ごとに空間の生物の配置状態を表示する。

では、これら仮定を満たすためにどのようなクラスを定義すればよいか考えてみましょう。

生物クラスの実装

はじめに、生物をどのように実現するか、を考える必要があります。要件を見る限り、生物ごと個体ごとに独立した状態を持ち、それぞれが個別に繁殖を行わなければなりません。 従って、直感的に1オブジェクトが1個体となるクラスを定義すれば良さそうです。

シミュレーションプログラムの今後のことを考えると、さまざまな生物をシミュレーションできるように新たな生物を追加しやすいプログラム構造の方が良い(拡張性が高いほうが良い)です。 さまざまな生物を同一の枠組みで実現するためには、その元となる生物クラスはなるべく抽象的である必要があります。 そのためには、先に述べた概念の階層を使って、生物と呼ぶために最低限必要となる抽象的な振る舞いを持った抽象的な生物クラスを作り、そのクラスを継承する形で、具体的な生物の固有の振る舞いを実装したクラスを生物種ごとに定義していく方針が良さそうです。

シミュレーションにおいて最低限必要となりそうな生物の振る舞いを抽出すると、以下になります。

  • 成長する。
  • 新しい個体を生み出す。
  • 個体を何らかの方法で表す。
  • 歳を取る。
  • 年齢を取得する。
  • 死ぬ。
  • 死んでいるかどうか判定する。

ここで、「個体を何らかの方法で表す」振る舞いは、例えば「個体の画像として表す」であったり「個体を文字として表す」など考えられます。今回は、ターミナルを使ったプログラムのため、「1文字として個体を表す」振る舞いとします。

さて、これら抽象的な生物クラスのために抽出した不可欠な振る舞いは、すべて抽象的でしょうか。

答えはそうとは限りません。例えば、生物的な死については種ごとに特に違いがあるとは言えず、生物としてみても共通した具体的な同一の状態・振る舞いであると言えます。また、年齢に関する振る舞いついても同様です。抽象の対義語として、具体的に定義できることを具象と呼びます。これら振る舞いは具象的であると言えます。一方、それ以外については、明らかに種ごとに異なる振る舞いです。従って抽象的であると言えます。

なお、空間で隣り合うセルを参照して繁殖を行う都合上、生物自身が自分の位置を把握しておく必要があります。よって、以下の2つの具象的な振る舞いも出来なければなりません。

  • 自分の座標を記録する。
  • 記録した自分の座標を取得する。

以上を踏まえると、生物の振る舞いを改めて以下にまとめることができます。

  • 成長する。(抽象的)
  • 新しい個体を生み出す。(抽象的)
  • 個体を何らかの方法で表す。(抽象的)
  • 歳を取る。(具象的)
  • 年齢を取得する。(具象的)
  • 死ぬ。(具象的)
  • 死んでいるかどうか判定する。(具象的)
  • 自分の座標を記録する。(具象的)
  • 記録した自分の座標を取得する。(具象的)

ここまでの考察から、以上の振る舞いを、抽象的な生物クラスは持たなければなりません。 ここまで設計したクラス間の関連性をクラス図(Class diagram) で示しましょう。図11に生物クラスのクラス図を示します。オブジェクト指向プログラミングにおいては、クラスとクラスの関連構造を示すためにクラス図が利用されます。

設計した抽象的な生物のクラス

図11:設計した抽象的な生物のクラスを表すクラス図

生物クラスが持つべき振る舞いをまとめると図11のように記述できます。クラス図では、そのクラスがどのようなフィールド、メソッドを持つのか明示します。

さて、生物クラスは抽象クラスであり、このクラスを継承して具体的な種のクラスを作成することを検討しました。これまでの検討結果をクラス図で示すと、図12のようになります。

設計した抽象的な生物のクラスと具体的な種のクラスの関係

図12:抽象的な生物のクラスと具体的な種のクラスの関係

図12では、草と羊を具体的な種クラスとして生物クラスを継承して定義しています。クラス図においては、白抜き三角矢印が継承関係を表します。

図12のクラス図をJavaの実装で書き直すと、例えば、図13になります。

実装クラス図

図13:実装クラス図

クラス図のメソッドは実装として書き下すとき、図14のルールに則って記述します。

アクセシビリティ記号 メソッド名(引数名: 引数の型): 返り値の型
図14:クラス図でのメソッドの書き方

ここでアクセシビリティは表1のアクセシビリティ記号と対応しています。

表1:アクセシビリティ記号とアクセシビリティの対応
アクセシビリティアクセシビリティ記号
public+
private-

なお、 抽象クラスはクラス名を斜体 に 抽象メソッドはメソッド名を斜体 にします。

図13に基づいて、生物クラス Life をプログラムで定義すると図15になります。 ここで、クラス Life は、次の具体的な振る舞いをすることを補足しておきます。

  • コンストラクタで寿命(月齢)と初期の空間上のインデックスを設定します。
  • growOldメソッドを月齢寿命回呼び出すと、死亡状態を判定するisDeadメソッドがtrueを返すようになります。
  • dieメソッドを呼び出すと、isDeadメソッドがtrueを返すようになります。
/**
 * 生物を表します。
 * @author Me
 */
public abstract class Life {

    /**
     * 寿命(月齢)を表します。
     */
    private int lifetime;
    
    /**
     * 月齢を表します。
     */
    private int month;
    
    /**
     * 死んでいるかどうかを表します。
     */
    private boolean death;
    
    /**
     * 空間オブジェクト中の位置を表すインデックスです。
     */
    private int spaceIndex;
    
    /**
     * 生物個体を初期化します。
     * @param lifetime 月齢で表された寿命。
     * @param spaceIndex 空間配置のインデックス。
     */
    public Life(int lifetime, int spaceIndex) {
        
        this.lifetime = lifetime;
        this.month = 0;
        
        this.death = false;
        
        this.spaceIndex = spaceIndex;
    }
    
    /**
     * 年齢をヶ月(月齢)で返却します。
     * @return 月齢。
     */
    public int getMonth() {
        
        return this.month;
    }
    
    /**
     * 年齢を1ヶ月進めます。
     */
    public void growOld() {

        if (this.month >= this.lifetime) {

            this.death = true;

        } else {
            
            this.month = this.month + 1;
        }
    }
    
    /**
     * 死んでいるかどうか判定します。
     * @return trueならば死んでいます。falseならば生きています。
     */
    public boolean isDead() {
        
        return this.death;
    }
    
    /**
     * 強制的に死にます。
     */
    public void die() {
        
        this.death = true;
    }
    
    /**
     * 空間オブジェクト中で設定されているであろうインデックスを返却します。
     * @return 空間オブジェクト中のインデックス。
     */
    public int getIndex() {
        
        return this.spaceIndex;    
    }
    
    /**
     * 空間オブジェクト中で設定されているであろうインデックスを設定します。
     * @param index 空間オブジェクト中のインデックス。
     */
    public void setIndex(int index) {
        
        this.spaceIndex = index;
    }
    
    /**
     * 1ヶ月間の成長をします。
     */
    public abstract void grow();
    
    /**
     * 繁殖して、新たな個体を作り出します。
     * @return 新たな個体。
     */
    public abstract Life reproduce();
    
    /**
     * 個体を文字列で表現します。
     * @return 文字列で表現された個体。
     */
    @Override
    public abstract String toString();    
}
図15:生物を表す抽象クラス

草クラスの実装

種の枠組みが出来たので、つぎに具体的な生物クラスを定義してみましょう。ここでは、草クラスを作ってみたいと思います。

具体的な生物クラスは、 grow メソッド、 reproduce メソッド、そして toString メソッドを実装しなければなりません。

草クラスではどのような振る舞いにするべきか、簡潔に以下のように仮定します。

  • growメソッド(1ヶ月成長する)
    • 単に1ヶ月分加齢する。
      • 親クラスのgrowOldメソッドを呼び出すだけ。
  • reproduceメソッド(新しい別個体を生み出す)
    • 30%の確率で、新たな草オブジェクトを返却する。
  • toStringメソッド(1文字として個体を表す)
    • 文字列Wを返却する。

ここで、reproduceメソッドは確率に基づいた振る舞いをします。プログラムにおいては、擬似乱数を使うことで確率的な振る舞いを定義できます。

Javaのjava.util.Randomクラスは、現れる数値のばらつきに問題があるものの高速に乱数を生成できる擬似乱数生成器です。図16のように使うことで、乱数を生成できます。

Random rng = new Random(); // 擬似乱数生成器オブジェクトを生成します。
double d = rng.nextDouble(); // 0.0以上1.0未満の範囲でランダムに浮動小数点型のデータを返します。

if (d < 0.4) {

	System.out.println("40%の確率で表示されます。");

} else {
	System.out.println("60%の確率で表示されます。");
}
図16:擬似乱数生成器クラスの利用例

reproduceメソッドでは、図16のようにRandomクラスオブジェクトを使って、繁殖成功率30%の確率で新たな草オブジェクトを作成するようにします。


ここで、繁殖失敗した場合、reproduceメソッドは何を返却すべきでしょうか。

Cのプログラムなどでは、失敗・例外値としてNULLを返却することがよくあります。

Javaにおいても、参照型の初期値であるnullはどのような参照型変数にも代入できるため、失敗・例外値としてnullを返却することがあります。

しかし、誤ってnullが入った参照型変数を介してメソッドなどのメンバー呼び出しを行うと、例外NullPointerExceptionが発生してしまいます。これでは処理が中断してしまうため、なるべく失敗・例外値としてnullは使うべきではありません。

従って、失敗・例外値となる種を新たに定義してあげることでこれを回避してみましょう。図17のように空の状態を表すEmptyクラスを Life クラスを継承して定義します。

Lifeクラスを継承したEmptyクラス

図17:Lifeクラスを継承したEmptyクラス

図18にEmptyクラスの実装例を示します。Emptyクラスは基本的に何もしません。 従って、どのような振る舞いにするべきか、簡潔に以下のように仮定します。

  • growメソッド(1ヶ月成長する)
    • なにもしない。
  • reproduceメソッド(新しい別個体を生み出す)
    • 自分自身を返却する。
  • toStringメソッド(1文字として個体を表す)
    • 文字列.を返却する。

空の状態は不変です。死の状態を持ちません。しかし、便宜的に、空の状態が死んでいる状態か、あるいは生きている状態か、定義しておく必要があります。ここでは、生物が存在していない状態を表すということはすなわち死んでいる状態であると仮定して、isDeadメソッドは常にtrueを返却するとしておきます。

/**
 * 空(から)の状態を表します。
 * @author Me
 */
public class Empty extends Life {

    /**
     * 空間インデックスを持つ空の状態を生み出します。
     * @param spaceIndex 空間の配置を表すインデックスです。
     */
    public Empty(int spaceIndex) {
        
        super(0, spaceIndex);
        
        this.die();
    }

    /**
     * 1ヶ月間の成長をします。
     */
    @Override
    public void grow() {
        
    }

    /**
     * 繁殖して、新たな個体を作り出します。しかし、新たに生み出す必要がないので、自分自身を返します。
     * @return 自分自身を返却します。
     */
    @Override
    public Life reproduce() {
                
        // 新たに生み出す必要がないので、自分自身を返す。
        return this;
    }

    /**
     * 空の状態を表す記号を返却します。
     * @return 空の状態を表す記号。
     */
    @Override
    public String toString() {
        
        return ".";
    }
}
図18:空の状態を表すEmptyクラスの例

以上を踏まえると、図19のような草クラスを定義できます。

import java.util.Random;

/**
 * 草を表します。
 * @author Me
 */
public class Grass extends Life {

    /**
     * 乱数生成オブジェクトです。
     */
    private Random rng;

    /**
     * 草の繁殖成功率です。0.0~1.0で表します。
     */
    public static final double REPRODUCTIVE_RATE = 0.3;
    
    /**
     * 月齢による種の寿命を表します。
     */
    public static final int LIFETIME = 12;
        
    /**
     * 指定された空間インデックスを持つ、草を生み出します。
     * @param index 空間インデックス。
     */
    public Grass(int index) {
        
        // 寿命を月齢で12として設定
        super(LIFETIME, index);

        // 乱数生成オブジェクトを作成
        this.rng = new Random();
    }
    
    /**
     * 1ヶ月間の成長をします。
     */
    @Override
    public void grow() {

        this.growOld();
    }

    /**
     * 繁殖して、新たな個体を作り出します。新たな個体が作成される条件は、繁殖する個体が死んでいないこと、乱数が繁殖成功率の範囲内に収まること、です。
     * @return 新たな個体。作成できなかった場合は空(から)のオブジェクト。
     */
    @Override
    public Life reproduce() {
                
        int index = this.getIndex();
                
        if (this.rng.nextDouble() < REPRODUCTIVE_RATE && ! this.isDead()) {

            return new Grass(index);
        }
        
        return new Empty(index);
    }

    /**
     * 草を表す記号を返却します。
     * @return 草を表す記号。
     */
    @Override
    public String toString() {
        
        return "W";
    }
}
図19:具体的な草を表すGrassクラスの例

図19のGrassクラスにおいて、繁殖成功率を表すREPRODUCTIVE_RATEフィールドはstatic修飾子が付いた静的メンバーですが、さらに final修飾子 が付けられていることがわかると思います。

変数定義においてfinal修飾子は、その変数が値の再代入できないものであることを表します。つまり、final修飾子が付いた変数は初期化しかできません。

従って、static finalが付いたフィールドは定数(Constants) と呼ばれます。プログラム上で変更することがない0や1以外の決まりきった値、例えば、成功率や寿命などは定数として定義することが一般的です。