繁殖シミュレーション/抽象化

この節の要点

  • インタフェースや抽象メソッドなど(完全に)抽象的な振る舞いや概念は、クラスや抽象でないメソッドなど具体的な振る舞いや概念に依存すべきではなく、具体的な振る舞いや概念が抽象的な振る舞いや概念に依存すべきであるクラスの設計方針を依存性の逆転原則と呼ぶ。
  • オブジェクト思考プログラミングにおいて、抽象化と多態性を駆使することで保守性・再利用性が高まるプログラム設計が可能となる。

ここからは、インタフェースをどのように利用していくか、引き続き、生物の繁殖シミュレーションプログラムを拡張していきながら見てきましょう。

抽象化と多態性を活用した制御構造の分離

実は、ここまでのプログラムにおいて、問題点が1つあります。

それは、具体的な生物クラスを新たに作るたびに、OneDimensionalSpaceクラスのnextメソッドに、条件分岐などを追加して種の1ヶ月後の状態計算の処理を追加していかなければならない、という点です。

現時点では、具体的な生物クラスが2種類のみなので、問題がなさそうに感じるかも知れませんが、数十種類になってくると話が変わります。nextメソッドは肥大化し、さらにOneDimensionalSpaceクラスと具体的な生物種クラスとの結合度が高まって、保守性・再利用性が著しく低下します(図37)。

OneDimensionalSpaceクラスと種クラスの関連の増加

図37:OneDimensionalSpaceクラスと種クラスの関連の増加

この状況は、状態計算の処理がうまく抽象化されずOneDimentionalSpaceクラスの振る舞いとして定義されているため起こっていることです。従って、各生物ごとの1ヶ月後の状態を計算する振る舞いをOneDimentionalSpaceクラスから各生物クラスに分離して追加しましょう。各生物クラスに次の抽象的な振る舞いを検討します。

  • strategyメソッド(1ヶ月後の状態を計算する)
    • 現在の自身の状態と空間の状態から、次の1ヶ月後の状態を計算し、空間に生物オブジェクトを割り当てる。

明らかに、1ヶ月後の状態を計算する振る舞いでは、現在の空間オブジェクトと、1ヶ月後の空間オブジェクトを参照できなければなりません。従って、OneDimensionalSpaceオブジェクトを渡す必要があるわけですが、これは抽象メソッドstrategyが、具体的なクラスであるOneDimensionalSpaceクラスに依存することになります。

よいプログラムの設計において、 抽象的な振る舞いや概念が具象的な概念・実装に依存すべきではありません 。この原則を 依存性の逆転原則(Dependency Inversion Principle) [14]と呼びます。

依存(Dependency) とは、クラス間で、一方のクラスのフィールド・メソッドが他方のクラス実装で利用されている関係を指します。このような関係を uses-a 関係と呼びます。

例えば、OneDimensionalSpaceクラスはsetLifeメソッドを持ちますが、setLifeメソッドは引数で他クラスである Life クラスを利用しています。このとき、OneDimensionalSpaceクラスは Life クラスに依存している、と言えます。

これは具体的な概念・実装は、再利用や保守を行う際に、既に述べた通り、プログラムの変更の影響を直接受けることが大きく関わっています。 この影響は抽象化・多態性を用いることで、分離することができます。しかしながら、抽象クラスや抽象メソッドと言ったプログラム構造の内部が具象クラスに依存していた場合、その具象クラス変更の影響が波及します。したがってなるべく抽象クラスは具象クラスには依存しないほうが良いのです。

それではどのようにすべきかというと、インタフェースを使って、空間クラスを抽象化します。そして、抽象化したインタフェースに抽象メソッドstrategyが依存するようにしましょう。

空間と呼ばれる概念は抽象度が高いため、抽象的な振る舞い以外の具体的な振る舞いを定義しにくいです。よって、抽象クラスよりもインタフェースを使った抽象化をすべきです。空間インタフェースSpaceを考えてみましょう。

空間の抽象的な振る舞いを抽出すると以下になります。

  • 空間に生物を配置する。
  • 空間の座標から生物を取得する。

ここまでの議論をまとめると、図38の関係を示すことができます。

SpaceインタフェースとLifeクラスの依存関係

図38:SpaceインタフェースとLifeクラスの依存関係

図38における点線矢印が依存関係を表しています。ここでは、インタフェースSpaceと 抽象クラス Life が相互に依存することが解ると思います。

以上を踏まえて、図39のようなSpaceインタフェースを定義してみましょう。

/**
 * 空間を表します。
 * @author Me
 */
public interface Space {
    
    /**
     * 生物を空間に配置します。
     * @param life 配置する生物。
     */
    public void setLife(Life life);
    
    /**
     * 指定された空間インデックスに配置された生物を取得します。
     * @param i 空間インデックス。
     * @return 空間インデックスに割り当てられている生物オブジェクト。
     */
    public Life getLife(int i);
}
図39:Spaceインタフェースの定義例

既に、図30で示したOneDimensionalSpaceクラスには同じシグネチャのsetLifeメソッドとgetLifeメソッドが存在するため、新たに追加でこれらを実装する必要はなく、実装先インタフェースを指定するだけで良いです。

さて、肝心の生物クラスが持つstrategyメソッドですが、図38、図39で定義したSpaceインタフェースを利用して、図40のように定義できます。

/**
 * 繁殖戦略を行います。これまでの空間に配置された生物を元に、自身が次の世代の空間を生み出します。
 * @param now 今の空間。
 * @param next 次の空間。
 */
public abstract void strategy(Space now, Space next);
図40:Lifeクラスへのstrategyメソッドの定義例

strategyメソッドの引数は2つのSpaceインタフェースが指定されており、依存性の逆転原則が満たされていることがわかります。このstrategyメソッドを生物クラス Life に追加します。

抽象クラス Life に抽象メソッドstrategyを追加すると、そのサブクラスであるGrassクラス(そしてMustardクラス)とEmptyクラスにおいて、strategyメソッドを実装しなければなりません。

ですが、この実装内容は、過去にこれらクラスの振る舞いの処理フローを図29のように統合したことからもわかるように、OneDimensionalSpaceクラスにて実装したnextメソッドの処理内容から分離すればよいだけです。

Grassクラスのstrategyメソッドに分離した処理を図41に、Emptyクラスへ分離した処理を図42に示します。

/**
 * 繁殖戦略を行います。これまでの空間に配置された生物を元に、自身が次の世代の空間を生み出します。自身が死亡状態であれば、空間から取り除きます。自身の左側、あるいは右側が空の状態であれば、reproduceメソッドに基づいて新たに植物を生み出します。
 * @param now 現在の空間。
 * @param next 次の空間。
 */
@Override
public void strategy(Space now, Space next) {
    
    int spaceIndex = this.getIndex();
    int leftIndex = spaceIndex - 1;
    int rightIndex = spaceIndex + 1;
    
    // 生物オブジェクトを取得します。
    Life center = now.getLife(spaceIndex);
    Life left = now.getLife(leftIndex);
    Life right = now.getLife(rightIndex);

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

            // 次の空間では空(から)の状態にします。
            next.setLife(new Empty(spaceIndex));
            
        } else {
            // 生物オブジェクトが生きていた場合。

            next.setLife(center);
            
            Life leftNext = next.getLife(leftIndex);
            Life rightNext = next.getLife(rightIndex);
        
            if (left instanceof Empty && leftNext instanceof Empty) {
                // 現在の空間インデックスの左側の生物オブジェクトを繁殖させます。
                Life leftNew = this.reproduce();
                leftNew.setIndex(leftIndex);

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

                next.setLife(rightNew);
            }
        }
    }
}
図41:Grassクラス(またはMustardクラス)のstrategyメソッドの定義例
/**
 * 繁殖戦略を行います。これまでの空間に配置された生物を元に、自身が次の世代の空間を生み出します。
 * @param now 現在の空間。
 * @param next 次の空間。
 */
@Override
public void strategy(Space now, Space next) {
    
    int spaceIndex = this.getIndex();
    
    if (next.getLife(spaceIndex) instanceof Empty) {
        
        next.setLife(this);
    }
}
図42:Emptyクラスのstrategyメソッドの定義例

さて、ここまでOneDimensionalSpaceクラスの今後の肥大化を防ぐためにnextメソッドを分離してきましたが、nextメソッド自体はどのように書き換えるか、感の良いひとならばもうわかったかも知れません。

答えは、各セルに対する繰り返し文でstrategyメソッドを呼び出すように直すだけです。図43にその修正例を示します。

/**
 * 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 target = this.getLife(i);
        
        // 生物オブジェクトを1ヶ月分成長させます。
        target.grow();
        // 今の空間の状態を元に、繁殖戦略を行って、次の空間の状態を決めます。
        target.strategy(this, nextSpace);
        
    }
    
    return nextSpace;
}
図43:OneDimensionalSpaceクラスのnextメソッドの修正例

図30のnextメソッドと図43を見比べると、個別の処理が分離され、抽象化された処理のみの記述となり明らかに見やすくなったと思います。ここで、図43の16行目と18行目にて、多態性の効力が発揮されている、と言えるでしょう。オブジェクト指向プログラミングにおいて、このような抽象化と多態性を組み合わせた設計が保守性・再利用性を増加させることが感覚としてわかったでしょうか。

今後、具体的な生物種についての1ヶ月後の状態を計算する処理は、その種のstrategyメソッドに記述することになります。

インタフェースを駆使した役割の追加

前節にて、多態性を駆使した抽象化を行いましたが、最後に、表示機能を拡張してみましょう。現状では、空間オブジェクトのセルの状態が標準出力に表示されるだけです。ここに、図44のように各空間オブジェクトの世代を表示できるようにしてみましょう。

世代と空間の状態を表示させた実行例

図44:世代と空間の状態を表示させた実行例

ここでのポイントは、既存のクラスをあまり改変することなく拡張していくために、インタフェースを使う点です。

さて、そもそも今回のシミュレーションプログラムでは、図10のフローチャートで示されている通り、オブジェクトの次状態の計算と反映、そして表示が、繰り返し処理上で実施されています。

しかし、一般的に表示処理は時間がかかるため、繰り返し処理上ではオブジェクトの記録だけにとどめておき、図44のようにシミュレーション処理終了後などにまとめて表示が行われることが多いです。従って、ここではこれまでのシミュレーションプログラムを図45のフローに則って改変してみましょう。

改良したシミュレーションプログラムのフロー

図45:改良したシミュレーションプログラムのフロー

図45における、オブジェクトの記録をどのように行うか、というと、そもそもこれまで作成してきたシミュレーションプログラムではループの繰り返しごとに空間オブジェクトを新たに作成していますから、作成したオブジェクトをオブジェクト記録用として別途用意したリスト構造にループごとに追加していくだけで、他に影響なく記録できます。

そして、シミュレーションの繰り返しが終わった後に、リスト構造を繰り返しループで走査して、追加されている空間オブジェクトを1つづつ順に表示していけば、図45のフローが実現できます。

さて、表示機能は、様々な概念が兼ねる可能性がある機能です。よって、「表示ができる」概念を定義して抽象化したいと思います。この概念は抽象度が高いので具体的な振る舞いを定義することが難しいです。 よってまず、以下の振る舞いを持つPrinterインタフェースを定義します。

  • 自身を表示する(printメソッド)。
    • 標準出力に何らかの形でオブジェクトを表示させる。

表示機能を持たせたいクラスにPrinterインタフェースを実装しましょう。世代を表示する機能も同様にPrinterインタフェースを実装することで実現させます。ここではクラス名をGenerationPrinterとしましょう。既に表示機能を持っている空間クラスOneDimensionalSpaceについても同様で、抽象化できるように拡張しておきます。

先に述べたように、Printerインタフェースを実装したクラスのオブジェクトは、代入互換性があるPrinter型として扱うことができますから、ArrayListなどのによるPrinter型のリストオブジェクトに区別することなく追加できます。図46のようにこのリストオブジェクトの中身を先頭から1つづつ順に参照し、自身を表示するprintメソッドを実行すれば、順序立ってまとめて表示できるはずです。

Printer型リストオブジェクトの表示

図46:Printer型リストオブジェクトの表示

ここまでの検討を全てまとめたクラス図を図47に示します。

Printerインタフェースとその具象クラスの関係

図47:Printerインタフェースとその具象クラスの関係

以上を踏まえて、図48にPrinterインタフェースの定義例を示します。

/**
 * 表示することができる対象を表します。
 * @author Me
 */
public interface Printer {
    
    /**
     * 表示します。
     */
    public void print();
}
図48:Printerインタフェースの定義例

図47のとおりOneDimensionalSpaceクラスとGenerationPrinterクラスにPrinterインタフェースを実装しましょう。 ここで既に、図30で示したOneDimensionalSpaceクラスには同じシグネチャのprintメソッドが存在するため、新たに追加でこれらを実装する必要はなく、実装先インタフェースを指定するだけで良いです。

最後に世代を表示させるクラスGenerationPrinterの実装例を図49に示します。

/**
 * 世代表示です。
 * @author Me
 */
public class GenerationPrinter implements Printer {

    /**
     * 世代を表します。
     */
    private int gen;

    /**
     * 世代を設定します。
     * @param gen 世代。
     */
    public GenerationPrinter(int gen) {
        /* 中略 */
    }
    
    /**
     * 世代を表示します。
     */
    @Override
    public void print() {
        /* 中略 */
    }
}
図49:GenerationPrinterクラスの定義例

あとは、これまで定義したインタフェース・クラスを駆使して図45に則ったフローにmainメソッドをなおしてあげれば良いだけです。

改良したフローのmainメソッドの例を図50に示します。

public static void main(String[] args) {
    
    // 初期空間を生成します。
    int size = 50;
    
    OneDimensionalSpace space = new OneDimensionalSpace(size);
    
    // 草をインデックス0に配置します。
    space.setLife(new Grass(0));
    // 菜の花をインデックス3に配置します。
    space.setLife(new Mustard(3));
    
    // 12ヶ月 * 20 = 20年
    int iteratedMonth = 12*20;
    
    // 表示リストを初期化します。
    ArrayList<Printer> plist = new ArrayList<Printer>();
    
    // 世代を表示するオブジェクトを表示リストに追加します。
    plist.add(new GenerationPrinter(0));
    // 初期空間を表示リストに追加します。
    plist.add(space);
    
    // シミュレーションを行います。
    for (int i = 0; i < iteratedMonth; i = i + 1) {
                    
        // 次の世代の空間を計算します。
        space = space.next();
        
        // 世代表示を表示リストに追加します。
        plist.add(new GenerationPrinter(i+1));
        // 空間を表示リストに追加します。
        plist.add(space);
    }
    
    // シミュレーション結果を表示します。
    for (Printer p : plist) {
        
        p.print();
    }  
}
図50:mainメソッドの定義例

[14]: A. Shalloway, and J.R. Trott, ``- デザインパターンとともに学ぶ - オブジェクト指向のこころ'', 丸善出版 (2014)